面试分享(四).项目相关

项目相关

聊项目,画项目架构图,画一个用户从发起请求到接收到响应,中间经过哪些服务,每个服务做什么事情的流程图。

你个人有什么优势

讲项目中的难点、挑战,如何解决的,项目这一块会问的特别细

线上有没有遇到其他问题,如何处理的

说一个你了解最多的框架,说出你的理解

项目中监控报警机制如何做的,说说你的了解

服务A调用服务B,用户请求服务A,发现返回较慢,如何定位这个问题

防止服务器雪崩

服务雪崩效应是一种因 服务提供者 的不可用导致 服务调用者 的不可用,并将不可用 逐渐放大 的过程.如果所示:
image

上图中, A为服务提供者, B为A的服务调用者, C和D是B的服务调用者. 当A的不可用,引起B的不可用,并将不可用逐渐放大C和D时, 服务雪崩就形成了.

服务雪崩效应形成的原因

我把服务雪崩的参与者简化为 服务提供者 和 服务调用者, 并将服务雪崩产生的过程分为以下三个阶段来分析形成的原因:

  1. 服务提供者不可用
  2. 重试加大流量
  3. 服务调用者不可用

服务雪崩的每个阶段都可能由不同的原因造成, 比如造成 服务不可用 的原因有:

  • 硬件故障
  • 程序Bug
  • 缓存击穿
  • 用户大量请求

硬件故障可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问.
缓存击穿一般发生在缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时. 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用.
在秒杀和大促开始前,如果准备不充分,用户发起大量请求也会造成服务提供者的不可用.

而形成 重试加大流量 的原因有:

  • 用户重试
  • 代码逻辑重试

在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单.
服务调用端的会存在大量服务异常后的重试逻辑.
这些重试都会进一步加大请求流量.

最后, 服务调用者不可用 产生的主要原因是:

  • 同步等待造成的资源耗尽

当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.

服务雪崩的应对策略

针对造成服务雪崩的不同原因, 可以使用不同的应对策略:

  • 流量控制
  • 改进缓存模式
  • 服务自动扩容
  • 服务调用者降级服务

流量控制 的具体措施包括:

  • 网关限流
  • 用户交互限流
  • 关闭重试

因为Nginx的高性能, 目前一线互联网公司大量采用Nginx+Lua的网关进行流量控制, 由此而来的OpenResty也越来越热门.

用户交互限流的具体措施有:

  • 采用加载动画,提高用户的忍耐等待时间.
  • 提交按钮添加强制等待时间机制.

改进缓存模式 的措施包括:

  • 缓存预加载
  • 同步改为异步刷新

服务自动扩容 的措施主要有:

  • AWS的auto scaling

服务调用者降级服务 的措施包括:
资源隔离

  • 对依赖服务进行分类
  • 不可用服务的调用快速失败
  • 资源隔离主要是对调用服务的线程池进行隔离.

我们根据具体业务,将依赖服务分为: 强依赖和若依赖. 强依赖服务不可用会导致当前业务中止,而弱依赖服务的不可用不会导致当前业务的中止.

不可用服务的调用快速失败一般通过 超时机制, 熔断器 和熔断后的 降级方法 来实现.

缓存穿透,缓存击穿,缓存雪崩解决方案分析

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决方案

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决方案

缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

使用互斥锁(mutex key)

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。在redis2.6.1之前版本未实现setnx的过期时间,所以这里给出两种版本代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

//2.6.1前单机版本锁
String get(String key) {
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他线程休息50毫秒后重试
Thread.sleep(50);
get(key);
}
}
}

最新版本代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}

memcache代码

1
2
3
4
5
6
7
8
9
10
11
if (memcache.get(key) == null) {  
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}

“提前”使用互斥锁(mutex key)

在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。伪代码如下:

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
26
27
28
v = memcache.get(key);  
if (v == null) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
} else {
if (v.timeout <= now()) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
// extend the timeout for other threads
v.timeout += 3 * 60 * 1000;
memcache.set(key, v, KEY_TIMEOUT * 2);

// load the latest value from db
v = db.get(key);
v.timeout = KEY_TIMEOUT;
memcache.set(key, value, KEY_TIMEOUT * 2);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}
}

“永远不过期”

这里的“永远不过期”包含两层意思:

(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String get(final String key) {  
V v = redis.get(key);
String value = v.getValue();
long timeout = v.getTimeout();
if (v.timeout <= System.currentTimeMillis()) {
// 异步更新后台异常执行
threadPool.execute(new Runnable() {
public void run() {
String keyMutex = "mutex:" + key;
if (redis.setnx(keyMutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(keyMutex, 3 * 60);
String dbValue = db.get(key);
redis.set(key, dbValue);
redis.delete(keyMutex);
}
}
});
}
return value;
}

资源保护

采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。

四种解决方案:没有最佳只有最合适

解决方案 优点 缺点
简单分布式互斥锁(mutex key) 1. 思路简单2. 保证一致性 1. 代码复杂度增大2. 存在死锁的风险3. 存在线程池阻塞的风险
“提前”使用互斥锁 保证一致性 同上
不过期(本文) 异步构建缓存,不会阻塞线程池 1.不保证一致性。2. 代码复杂度增大(每个value都要维护一个timekey)。3. 占用一定的内存空间(每个value都要维护一个timekey)。
资源隔离组件hystrix(本文) 1.hystrix技术成熟,有效保证后端。2. hystrix监控强大。 部分访问存在降级策略。

怎么理解微服务,服务如何划分,可以从哪几个方面去划分,为什么这样划分,微服务带来了哪些好处,哪些坏处,如何看待这个问题?

image
优点

1:提升开发交流,每个服务足够内聚,足够小,代码容易理解;

2:服务独立测试、部署、升级、发布;

3:按需定制的DFX,资源利用率,每个服务可以各自进行x扩展和z扩展,而且,每个服务可以根据自己的需要部署到合适的硬件服务器上;每个服务按4:需要选择HA的模式,选择接受服务的实例个数;

5:容易扩大开发团队,可以针对每个服务(service)组件开发团队;

6:提高容错性(fault isolation),一个服务的内存泄露并不会让整个系统瘫痪;

7:新技术的应用,系统不会被长期限制在某个技术栈上;

缺点

没有银弹,微服务提高了系统的复杂度;开发人员要处理分布式系统的复杂性;服务之间的分布式通信问题;服务的注册与发现问题;服务之间的分布式事务问题;数据隔离再来的报表处理问题;服务之间的分布式一致性问题;服务管理的复杂性,服务的编排;不同服务实例的管理。

image

Chris Richardson提出的微服务的三维扩展模型:

X轴,服务实例水平扩展,保证可靠性与性能;

Y轴,功能的扩展,服务单一职责,功能独立;

Z轴,数据分区,数据独立,可靠性保证;

拆分例子

姿势一

新浪微博微服务专家胡忠想从纵横两个维度来划分,简单粗暴:

1.1 纵向拆分

  从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。

1.2 横向拆分

  从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。

  纵向以业务为基准,关系铁的在一起;横向功能独立的在一起。我想如果拆分这么简单,你有底气拆,敢拆吗?所以我们又继续比对一下其他专家的言论。
  image

姿势二

阿里的小伙伴从综合的维度来看,部分维度和上面会有重合。

2.1 服务拆分要迎合业务的需要

  充分考虑业务独立性和专业性,避免以团队来定义服务边界,从而出现“土匪”抢地盘,影响团队信任。

  这个维度和上面的类似,但是强调的是业务和团队成员的各自独立性,对上面是一种很好的补充。

2.2 拆分后的维护成本要低于拆分前

  这里的维护成本包括:人力、物力、时间。

  这里的成本对大部分中小团队来说都是必须要考虑的重要环节,如果投入和收益不能成正比,或者超出领导的预算或者市场窗口,那么先进的技术就是绊脚石,千万不要迷恋技术,所谓工程师思维千万要不得。

2.3 拆分不仅仅是架构的调整,组织结构上也要做响应的适应性优化

  确保拆分后的服务由相对独立的团队负责维护。

  这句话怎么理解呢?传统的团队划分是按照产品部、前端、后端横向划分,微服务化以后的团队可能就会是吃一张披萨饼的人数,产品、前端、后端被归类到服务里面,以服务为中心来分配人数。

2.4 拆分最有价值的结果是提高了系统的可扩展性

  把具有不同扩展性要求的服务拆分出来,分别进行部署,降低成本,提高效率。比如全文搜索服务。

  这点和上面的按功能独立性来拆分有点类似,功能独立其实就是面向可扩展性。

2.5 考虑软件发布频率

  比如把20%经常变动的部分进行抽离,80%不经常变动的单独部署和管理。说白了就是按照8/2原则进行拆分。这个拆分的好处很明显,可以尽可能的减少发布产生的后遗症,比如用户体验、服务相互干扰等。

  但是这里有一个问题,假如20%的服务分属于不同的业务层面,那该怎么办?所以这里的拆分应该有个优先级,在拆分相互冲突的时候应该要优先考虑权重比较高的那个。
  image   

姿势三

资深技术专家李运华在他的架构书中给出的拆分:

3.1 基于业务逻辑

  将系统中的业务按照职责范围进行识别,职责相同的划分为一个单独的服务。这种业务优先的方式在前面两种姿势当中都出现过,可见是最基本,最重要的划分方式(没有之一)。

3.2 基于稳定性

  将系统中的业务模块按照稳定性进行排序。稳定的、不经常修改的划分一块;将不稳定的,经常修改的划分为一个独立服务。比如日志服务、监控服务都是相对稳定的服务,可以归到一起。这个很类似上面提到的2/8原则,80%的业务是稳定的。

  至此你会发现服务的拆分真的没有绝对的标准,只有合理才是标准。

3.3 基于可靠性

  同样,将系统中的业务模块按照可靠性进行排序。对可靠性要求比较高的核心模块归在一起,对可靠性要求不高的非核心模块归在一块。

  这种拆分的高明可以很好的规避因为一颗老鼠屎坏了一锅粥的单体弊端,同时将来要做高可用方案也能很好的节省机器或带宽的成本。

3.4 基于高性能

  同上,将系统中的业务模块按照对性能的要求进行优先级排序。把对性能要求较高的模块独立成一个服务,对性能要求不高的放在一起。比如全文搜索,商品查询和分类,秒杀就属于高性能的核心模块。

image

如何理解网关,网关带来的好处和坏处,如何解决

API网关我的分析中会用到以下三种场景。

  • Open API。企业需要将自身数据、能力等作为开发平台向外开放,通常会以rest的方式向外提供,最好的例子就是淘宝开放平台、腾讯公司的QQ开放平台、微信开放平台。 Open API开放平台必然涉及到客户应用的接入、API权限的管理、调用次数管理等,必然会有一个统一的入口进行管理,这正是API网关可以发挥作用的时候。
  • 微服务网关。微服务的概念最早在2012年提出,在Martin Fowler的大力推广下,微服务在2014年后得到了大力发展。 在微服务架构中,有一个组件可以说是必不可少的,那就是微服务网关,微服务网关处理了负载均衡,缓存,路由,访问控制,服务代理,监控,日志等。API网关在微服务架构中正是以微服务网关的身份存在。
  • API服务管理平台。上述的微服务架构对企业来说有可能实施上是困难的,企业有很多遗留系统,要全部抽取为微服务器改动太大,对企业来说成本太高。但是由于不同系统间存在大量的API服务互相调用,因此需要对系统间服务调用进行管理,清晰地看到各系统调用关系,对系统间调用进行监控等。 API网关可以解决这些问题,我们可以认为如果没有大规模的实施微服务架构,那么对企业来说微服务网关就是企业的API服务管理平台。

一个企业随着信息系统复杂度的提高,必然出现外部合作伙伴应用、企业自身的公网应用、企业内网应用等,在架构上应该将这三种应用区别开,三种应用的安排级别、访问方式也不一样。 因此在我的设计中将这三种应用分别用不同的网关进行API管理,分别是:API网关(OpenAPI合伙伙伴应用)、API网关(内部应用)、API网关(内部公网应用)。

image

对于OpenAPI使用的API网关来说,一般合作伙伴要以应用的形式接入到OpenAPI平台,合作伙伴需要到 OpenAPI平台申请应用。 因此在OpenAPI网关之外,需要有一个面向合作伙伴的使用的平台用于合作伙伴,这就要求OpenAPI网关需要提供API给这个用户平台进行访问。 如下架构:

image

对于内网的API网关,在起到的作用上来说可以认为是微服务网关,也可以认为是内网的API服务治理平台。 当企业将所有的应用使用微服务的架构管理起来,那么API网关就起到了微服务网关的作用。 而当企业只是将系统与系统之间的调用使用rest api的方式进行访问时使用API网关对调用进行管理,那么API网关起到的就是API服务治理的作用。

image
对于公司内部公网应用(如APP、公司的网站),如果管理上比较细致,在架构上是可能由独立的API网关来处理这部分内部公网应用,如果想比较简单的处理,也可以是使用面向合作伙伴的API网关。 如果使用独立的API网关,有以下的好处:

  • 面向合作伙伴和面向公司主体业务的优先级不一样,不同的API网关可以做到业务影响的隔离。
  • 内部API使用的管理流程和面向合作伙伴的管理流程可能不一样。
  • 内部的API在功能扩展等方面的需求一般会大于OpenAPI对于功能的要求。
    基于以上的分析,如果公司有能力,那么还是建议分开使用合作伙伴OPEN API网关和内部公网应用网关

API网关有哪些竞争方案

1、对于Open API平台的API网关,我分析只能选择API网关作为解决方案,业界没有发现比较好的可以用来作为Open API平台的入口的其他方案。

2、对于作为微服务网关的API网关,业界的选择可以选择的解决方案比较多,也取决于微服务器的实现方案,有一些微服务架构的实现方案是不需要微服务网关的。

  • Service Mesh,这是新兴的基于无API网关的架构,通过在客户端上的代理完成屏蔽网络层的访问,这样达到对应用层最小的改动,当前Service Mesh的产品还正在开发中,并没有非常成熟可直接应用的产品。 发展最迅速的产品是Istio。 建议大家密切关注相关产品的研发、业务使用进展。
    image
  • 基于duboo架构,在这个架构中通常是不需要网关的,是由客户端直接访问服务提供方,由注册中心向客户端返回服务方的地址。
    image

API网关解决方案

私有云开源解决方案如下:

公有云解决方案:

自开发解决方案:

  • 基于Nginx+Lua+ OpenResty的方案,可以看到Kong,orange都是基于这个方案
  • 基于Netty、非阻塞IO模型。 通过网上搜索可以看到国内的宜人贷等一些公司是基于这种方案,是一种成熟的方案。
  • 基于Node.js的方案。 这种方案是应用了Node.js天生的非阻塞的特性。
  • 基于java Servlet的方案。 zuul基于的就是这种方案,这种方案的效率不高,这也是zuul总是被诟病的原因。

怎么选择API网关

如果是要选择一款已有的API网关,那么需要从以下几个方面去考虑。

1、性能与可用性
如果一旦采用了API网关,那么API网关就会作为企业应用核心,因此性能和可用性是必须要求的。

从性能上来说,需要让网关增加的时间消耗越短越好,个人觉得需要10ms以下。 系统需要采用非阻塞的IO,如epoll,NIO等。网关和各种依赖的交互也需要是非阻塞的,这样才能保证整体系统的高可用性,如:Node.js的响应式编程和基于java体现的RxJava和Future。
网关必须支持集群部署,任务一台服务器的crash都应该不影响整体系统的可用性。

多套网关应该支持同一管理平台和同一监控中心。 如: 一个企业的OpenAPI网关和内部应用的多个系统群的不同的微服务网关可以在同一监控中心进行监控。

2、可扩展性、可维护性

一款产品总有不能满足生产需求的地方,因此需求思考产品在如何进行二次开发和维护,是否方便公司团队接手维护产品。

3、需求匹配度

需要评估各API网关在需求上是否能满足,如: 如果是OpenAPI平台需要使用API网关,那么需要看API网关在合作伙伴应用接入、合作伙伴门户集成、访问次数限额等OpenAPI核心需求上去思考产品是否能满足要求。 如果是微服务网关,那么要从微服务的运维、监控、管理等方面去思考产品是否足够强大。

4、是否开源?公司是否有自开发的能力?

现有的开源产品如kong,zuul,orange都有基础的API网关的核心功能,这些开源产品大多离很好的使用有一定的距离,如:没有提供管理功能的UI界面、监控功能弱小,不支持OpenAPI平台,没有公司运营与运维的功能等。 当然开源产品能获取源代码,如果公司有比较强的研发能力,能hold住这些开源产品,经过二次开发kong、zuul应该还是适应一些公司,不过需求注意以下一些点:

kong是基于ngnix+lua的,从公司的角度比较难于找到能去维护这种架构产品的人。 需求评估当前公司是否有这个能力去维护这个产品。

zuul因为架构的原因在高并发的情况下性能不高,同时需要去基于研究整合开源的适配zuul的监控和管理系统。
orange由于没有被大量使用,同时是国内个人在开源,在可持续性和社区资源上不够丰富,出了问题后可能不容易找到人问。
另外kong提供企业版本的API网关,当然也是基于ngnix+lua的,企业版本可以购买他们的技术支持、培训等服务、以及拥有界面的管理、监控等功能。

5、公有云还是私有云

现在的亚马逊、阿里、腾讯云都在提供基础公有云的API网关,当然这些网关的基础功能肯定是没有问题,但是二次开发,扩展功能、监控功能可能就不能满足部分用户的定制需求了。另外很多企业因为自身信息安全的原因,不能使用外网公有网的API网关服务,这样就只有选择私有云的方案了。
在需求上如果基于公有云的API网关只能做到由内部人员为外网人员申请应用,无法做到定制的合作伙伴门户,这也不适合于部分企业的需求。
如果作为微服务网关,大多数情况下是希望网关服务器和服务提供方服务器是要在内网的,在这里情况下也只有私有云的API网关才能满足需求。

综合上面的分析,基础公有云的API网关只有满足一部分简单客户的需求,对于很多企业来说私有云的API网关才是正确的选择。

项目中如何保证接口的幂等操作

  • 调用者给消息一个唯一请求ID标识。ID标识一个工作单元,这个工作单元只应执行一次,工作单元ID可以是Schema的一部分,也可以是一个定制的SOAP Header,服务的Contract 可以说明这个唯一请求ID标识是必须的
  • 接收者在执行一个工作单元必须先检验该工作单元是否已经执行过。检查是否执行的逻辑通常是根据唯一请求ID ,在服务端查询请求是否有记录,是否有对应的响应信息,如果有,直接把响应信息查询后返回;如果没有,那么就当做新请求去处理

遇到过线上服务器CPU飙高的情况没有,如何处理的?

  • 登录服务器,执行top命令,查看CPU占用情况(top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器,通过以上命令,我们可以看到,进程ID为1893的Java进程的CPU占用率达到了181%,基本可以定位到是我们的Java应用导致整个服务器的CPU占用率飙升)

    1
    2
    3
    $top
    PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
    1893 admin 20 0 7127m 2.6g 38m S 181.7 32.6 10:20.26 java
  • Java是单进程多线程的,那么,我们接下来看看PID=1893的这个Java进程中的各个线程的CPU使用情况,同样是用top命令,通过top -Hp 1893命令,我们可以发现,当前1893这个进程中,ID为4519的线程占用CPU最高。

    1
    2
    3
    $top -Hp 1893
    PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
    4519 admin 20 0 7127m 2.6g 38m R 18.6 32.6 0:40.11 java
  • 通过top命令,我们目前已经定位到导致CPU使用率较高的具体线程, 那么我么接下来就定位下到底是哪一行代码存在问题。首先,我们需要把4519这个线程转成16进制

    1
    2
    $printf %x 4519
    11a7
  • 接下来,通过jstack命令,查看栈信息(通过以上代码,我们可以清楚的看到,BeanValidator.java的第30行是有可能存在问题的):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $sudo -u admin  jstack 1893 |grep -A 200 11a7
    "HSFBizProcessor-DEFAULT-8-thread-5" #500 daemon prio=10 os_prio=0 tid=0x00007f632314a800 nid=0x11a2 runnable [0x000000005442a000]
    java.lang.Thread.State: RUNNABLE
    at sun.misc.URLClassPath$Loader.findResource(URLClassPath.java:684)
    at sun.misc.URLClassPath.findResource(URLClassPath.java:188)
    at java.net.URLClassLoader$2.run(URLClassLoader.java:569)
    at java.net.URLClassLoader$2.run(URLClassLoader.java:567)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findResource(URLClassLoader.java:566)
    at java.lang.ClassLoader.getResource(ClassLoader.java:1093)
    at java.net.URLClassLoader.getResourceAsStream(URLClassLoader.java:232)
    at org.hibernate.validator.internal.xml.ValidationXmlParser.getInputStreamForPath(ValidationXmlParser.java:248)
    at org.hibernate.validator.internal.xml.ValidationXmlParser.getValidationConfig(ValidationXmlParser.java:191)
    at org.hibernate.validator.internal.xml.ValidationXmlParser.parseValidationXml(ValidationXmlParser.java:65)
    at org.hibernate.validator.internal.engine.ConfigurationImpl.parseValidationXml(ConfigurationImpl.java:287)
    at org.hibernate.validator.internal.engine.ConfigurationImpl.buildValidatorFactory(ConfigurationImpl.java:174)
    at javax.validation.Validation.buildDefaultValidatorFactory(Validation.java:111)
    at com.test.common.util.BeanValidator.validate(BeanValidator.java:30)
  • 线上问题排查还可以使用Alibaba开源的工具Arthas进行排查,以上问题,可以使用一下命令定位thread -n 3

如果线上一个功能是用栈结构实现的,使用过程中要注意哪些问题,为什么?

  • stack栈:是自动分配变量,以及函数调用的时候所使用的一些空间。地址是由高向低减少的。由编译器自动分配内存空间,代码结束,自动释放内存空间。它的内部结构是一个容器式,只有一个进口,并且也作为出口。这就是导致,先进来的数据在“容器”底部,后进来的数据在“容器顶部”,出栈的时候,就应验了“后进先出,先进后出”的这句话。
  • 对于栈来说,理论上线性表的操作特性它都具备,可由于它的特殊性,所以针对它的操作上会有些变化。特别是插入和删除操作,我们改名为push和pop,进栈和出栈。
  • 主要用途:函数调用和返回,数字转字符,表达式求值,走迷宫等等。只要数据的保存满足先进后出的原理,都优先考虑使用栈,所以栈是计算机中不可缺的机制。
  • 建立Stack数组时需要为每一个Stack元素初始化一个对象

    分布式锁的实现、对比Redis分布式锁 & ZK分布式锁

  • redis分布式锁
    • 简单算法:是redis官方支持的分布式锁算法。这个分布式锁有3个重要的考量点,互斥(只能有一个客户端获取锁),不能死锁,容错(大部分redis节点或者这个锁就可以加可以释放),第一个最普通的实现方式,如果就是在redis里创建一个key算加锁。SET my:lock 随机值 NX PX 30000,这个命令就ok,这个的NX的意思就是只有key不存在的时候才会设置成功,PX 30000的意思是30秒后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。为啥要用随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除key的话会有问题,所以得用随机值加上面的lua脚本来释放锁。但是这样是肯定不行的。因为如果是普通的redis单实例,那就是单点故障。或者是redis普通主从,那redis主从异步复制,如果主节点挂了,key还没同步到从节点,此时从节点切换为主节点,别人就会拿到锁。
      • RedLock算法:这个场景是假设有一个redis cluster,有5个redis master实例。然后执行如下步骤获取一把锁
        1. 获取当前时间戳,单位是毫秒;
        2. 跟上面类似,轮流尝试在每个master节点上创建锁,过期时间较短,一般就几十毫秒;
        3. 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1);
        4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
        5. 要是锁建立失败了,那么就依次删除这个锁;
        6. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。
        
  • zk分布式锁
    • zk分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新枷锁。
  • redis分布式锁和zk分布式锁的对比:redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能;zk分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。另外一点就是,如果是redis获取锁的那个客户端bug了或者挂了,那么只能等待超时时间之后才能释放锁;而zk的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁。redis分布式锁大家每发现好麻烦吗?遍历上锁,计算时间等等。zk的分布式锁语义清晰实现简单。所以先不分析太多的东西,就说这两点,我个人实践认为zk的分布式锁比redis的分布式锁牢靠、而且模型简单易用。

项目中系统监控怎么做的

  • 集中式日志系统
    • Elasticsearch
    • Logstash
    • Kibana
    • Beats
  • 集中式度量系统
    • Prometheus
    • Cat
  • 分布式追踪系统
    • Zipkin
    • Pinpoint
    • Skywalking
    • Jaeger

如何实现的,Snowflake实现原理,Snowflake有哪些问题,如何避免根据订单号可以推算出今天的订单量

  • 正数位(占1比特)+时间戳(占41比特)+机械id(占5比特)+数据中心(占5比特)+自增值(占12比特),总共64比特组成的一个Long类型。
  • 时间戳(占41个比特):毫秒数,大约可以使使用69年
  • 机械id(占5个比特):即2的5次方等于32个机器
  • 数据中心id(占5个比特):即2的5次方等于32个数据中心
  • 自增值(占12比特):2的12次方等于4096。也就是说每毫秒最多可以生成4096个id,如果cpu生产id的速度大于每毫秒4096个,那么需要使线程进行等待到下一毫秒,重新计数获取自增值。
  • 好处
    • 生成的id是一个数字的Long类型
    • 无需链接数据库或者redis,超高性能
  • 弊端
    • 每毫秒只能生成4096个id。随着cpu不断的进步,每毫秒4096个id将不能满足。可以不用担心,即便cpu性能超过了这个值,那么只需等待到下一个毫秒
    • 只能使用69年
    • 每毫秒重新计数,空闲时间会浪费很多id空间
    • 系统时间不可回退,回退将会导致id重复。另:系统时间可以前进,不受影响

如何设计一个秒杀系统?

  • 同样是高并发场景,三类业务的架构挑战不一样
    • QQ类业务,用户主要读写自己的数据,访问基本带有uid属性,数据访问锁冲突较小
    • 微博类业务,用户的feed主页由别人发布的消息构成,数据读写有一定锁冲突
    • 12306类业务,并发量很高,几乎所有的读写锁冲突都集中在少量数据上,难度最大
  • 将请求尽量拦截在系统上游,而不要让锁冲突落到数据库。传统秒杀系统之所以挂,是因为请求都压到了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,访问流量大,下单成功的有效流量小。一趟火车2000张票,200w个人同时来买,没有人能买成功,请求有效率为0。画外音:此时系统的效率,还不如线下售票窗口。
  • 充分利用缓存,秒杀买票,这是一个典型的读多写少的业务场景:
    • 车次查询,读,量大
    • 余票查询,读,量大
    • 下单和支付,写,量小
      一趟火车2000张票,200w个人同时来买,最多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%,非常适合使用缓存来优化。
  • 秒杀业务,常见的系统分层架构

image

  • 秒杀业务,可以使用典型的服务化分层架构

    • 端(浏览器/APP),最上层,面向用户
      • JS层面,可以限制用户在x秒之内只能提交一次请求,从而降低系统负载。
      • APP层面,可以做类似的事情,虽然用户疯狂的在摇微信抢红包,但其实x秒才向后端发起一次请求。
      • 不过,端上的拦截只能挡住普通用户(99%的用户是普通用户),程序员firebug一抓包,写个for循环直接调用后端http接口,js拦截根本不起作用,这下怎么办?
    • 站点层,访问后端数据,拼装html/json返回,如何抗住程序员写for循环调用http接口,首先要确定用户的唯一标识,对于频繁访问的用户予以拦截。
      • 购票类业务都需要登录,用uid就能标识用户,在站点层,对同一个uid的请求进行计数和限速,例如:一个uid,5秒只准透过1个请求,这样又能拦住99%的for循环请求。
      • 一个uid,5s只透过一个请求,其余的请求怎么办,缓存,页面缓存,5秒内到达站点层的其他请求,均返回上次返回的页面。
      • OK,通过计数、限速、页面缓存拦住了99%的普通程序员,但仍有些高端程序员,例如黑客,控制了10w个肉鸡,手里有10w个uid,同时发请求,这下怎么办
    • 服务层的请求拦截

      • 服务层非常清楚业务的库存,非常清楚数据库的抗压能力,可以根据这两者进行削峰限速。例如,业务服务很清楚的知道,一列火车只有2000张车票,此时透传10w个请求去数据库,是没有意义的。用什么削峰?请求队,对于写请求,做请求队列,每次只透传有限的写请求去数据层(下订单,支付这样的写业务)。只有2000张火车票,即使10w个请求过来,也只透传2000个去访问数据库:

        • 如果前一批请求均成功,再放下一批
        • 如果前一批请求库存已经不足,则后续请求全部返回“已售罄”

          对于读请求,怎么优化?
          cache抗,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的。
          如此削峰限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去,又有99%的请求被拦住了。

    • 数据库层,
      经过前三层的优化:

      • 浏览器拦截了80%请求
      • 站点层拦截了99%请求,并做了页面缓存
      • 服务层根据业务库存,以及数据库抗压能力,做了写请求队列与数据缓存

        你会发现,每次透到数据库层的请求都是可控的。
        db基本就没什么压力了,闲庭信步。此时,透2000个到数据库,全部成功,请求有效率100%。

    • 按照上面的优化方案,其实压力最大的反而是站点层,假设真实有效的请求数是每秒100w,这部分的压力怎么处理?
      • 站点层水平扩展,通过加机器扩容,一台抗5000,200台搞定;
      • 服务降级,抛弃请求,例如抛弃50%;
      • 原则是要保护系统,不能让所有用户都失败。
    • 站点层限速,是个每个uid的请求计数放到redis里么?吞吐量很大情况下,高并发访问redis,网络带宽会不会成为瓶颈?
      • 同一个uid计数与限速,如果担心访问redis带宽成为瓶颈,可以这么优化
        • 计数直接放在内存,这样就省去了网络请求;
        • 在nginx层做7层均衡,让一个uid的请求落到同一个机器上;
  • 除了系统上的优化,产品与业务还能够做一些折衷,降低架构难度。
    • 业务折衷一,一般来说,下单和支付放在同一个流程里,能够提高转化率。对于秒杀场景,产品上,下单流程和支付流程异步,放在两个环节里,能够降低数据库写压力。以12306为例,下单成功后,系统占住库存,45分钟之内支付即可。
    • 业务折衷二,一般来说,所有用户规则相同,体验会更好。对于秒杀场景,产品上,不同地域分时售票,虽然不是所有用户规则相同,但能够极大降低系统压力。北京9:00开始售票,上海9:30开始售票,广州XX开始售票,能够分担系统压力。
    • 业务折衷三,秒杀场景,由于短时间内并发较大,系统返回较慢,用户心情十分焦急,可能会频繁点击按钮,对系统造成压力。产品上可以优化为,一旦点击,不管系统是否返回,按钮立刻置灰,不给用户机会频繁点击。
    • 业务折衷四,一般来说,显示具体的库存数量,能够加强用户体验。对于秒杀场景,产品上,只显示有/无车票,而不是显示具体票数目,能够降低缓存淘汰率。
      画外音:显示库存会淘汰N次,显示有无只会淘汰1次。更多的,用户关注是否有票,而不是票有几张。
  • 总结,对于秒杀系统,除了产品和业务上的折衷,架构设计上主要有两大优化方向
    • 尽量将请求拦截在系统上游;
    • 读多写少用缓存;

如果我现在就是要实现每秒10w请求,不能熔断限流,如何去设计?

  • Cache住所有查询,两层cache:除了使用ckv做全量缓存,还在数据访问层dao中增加本机内存cache做二级缓存,cache住所有读请求。查询失败或者查询不存在时,降级内存cache;内存cache查询失败或记录不存在时降级DB。DB本身不做读写分离。
  • DB写同步cache,容忍少量不一致。DB写操作完成后,dao中同步内存cache,业务服务层同步ckv,失败由异步队列补偿,定时的ckv与DB备机对账,保证最终数据一致。

服务A调用服务B中一个接口,服务B调用服务C中一个接口,如何实现若服务B响应服务A成功,则服务C一定响应服务B成功,需要考虑系统性能问题?

欢迎补充。。。。。

一致性hash

一致性哈希(Consistent hashing)算法是由 MIT 的Karger 等人与1997年在一篇学术论文(《Consistent hashing and random trees: distributed caching protocols for relieving hot spots on the World Wide Web》)中提出来的,用于解决分布式缓存数据分布问题。在传统的哈希算法下,每条缓存数据落在那个节点是通过哈希算法和服务器节点数量计算出来的,一旦服务器节点数量发生增加或者介绍,哈希值需要重新计算,此时几乎所有的数据和服务器节点的对应关系也会随之发生变化,进而会造成绝大多数缓存的失效。一致性哈希算法通过环形结构和虚拟节点的概念,确保了在缓存服务器节点数量发生变化时大部分数据保持原地不动,从而大幅提高了缓存的有效性。下面我们通过例子来解释一致性哈希的原理。

比如有 n 个节点,对于缓存 数据(k,v)具体存在哪个节点往往 hash(k) % n 来计算处理,举一个例子如下表所示,一共有个3个节点,hash函数采用 md5 。

缓存key hash(k) % n 服务器节点
user_nick_rommel 1 192.168.56.101
user_nick_pandy 0 192.168.56.100
user_nick_sam 2 192.168.56.102

Md5 的计算结果一般是一串32位的16进制字符串,做取模运算时原始数字较长,实际使用时,可以只截取最后4位或者8位使用,因为hash函数具有随机性,当数据量足球大时,截取部分数据也能保证数据的均匀分布。比如 md5(‘user_nick_rommel’),对应字符串为 29e4fd2a0f05bd63343ae2276ca5038e,取最后4位 038e 转成10进制整数在进行取模运算,038e 对应的10进制数为 910,取模计算得 1 (9102 % 3 = 1)。

如果此时对缓存服务器进行扩容,添加一个新节点如 192.168.56.103,那么按照上面的计算方式,n 变为4, 得到的结果如下:

缓存key hash(k) % n 服务器节点
user_nick_rommel 2 192.168.56.102
user_nick_pandy 2 192.168.56.102
user_nick_sam 3 192.168.56.103

从结果中可见,缓存对应关系完全发生改变,比如 user_nick_rommel 这个可以,添加节点钱可以从 192.168.56.101 中读到,添加节点后却读不到了,。一般缓存失效时应用程序都会重新从后端服务加载数据(比如数据库),以这种这种方式分配缓存,当缓节点数量发生改变时,会造成大面积的缓存失效,这回造成后端服务瞬间压力上升,压力过大会造成服务不可以用,如果服务出于关键节点,甚至还会引发雪崩效应(TODO)。

在实际应用中,缓存节点由于故障挂掉,或者空间不足而进行扩容,缓存节点的增减是比较常见的事情,但上面传统方式会使服务的不可靠,下面看下一致性哈希是如何解决这个问题的。

在一致性哈希算法中,首先将哈希空间映射到一个虚拟的环上,环上的数值分从 0 到 2^32-1(哈希值的范围),如下图:

image

在一致性哈希算法刚提出来的时候,32位系统还是主流,2^32-1 相当于最大Integer,现在的应用服务器普遍都是64位系统,在使用使用一致性哈希算法时可以根据实际情况适当变通,比如将哈希值空间放大到 2^64-1。

然后使用同样的哈希算法将缓存服务节点(通常通过服务器IP+端口作为节点的key)和数据键映射到环上的位置。再决定数据落在那台服务器上时,使用一致的方向(比如顺时针方向)沿环查找,遇到的第一个有效服务器就是缓存保存的地方,如图:

image

当有新的服务器节点加入时,按照同样的哈希算法将新节点映射到环上的某处位置,和新节点相邻的数据逆时针节点会进行迁移,其他节点保持不变。如下图,当新加入一台新服务器 192.168.56.104 时,user_nick_pandy 这条缓存数据的请求根据算法会落在192.168.56.104 这台及其上,其他节点不受任何影响。

image

另外,由于哈希计算的记过通常都比较随机,如果缓存服务器比较少的话,可能会出现数据分配冷热不均的问题,如下图所示,大部分数据都会存储在 node3 节点上。

image

为了解决这个问题,我们引入虚拟节点的概念,在实体服务器不增加的情况下,用多个虚拟节点替代原来的单个实体节点,一台服务服务器在环上就对应多个位置,这样可以让数据存储更加均匀,各服务器的负载页更加平衡,如图:

image

使用 Java 代码实现一致性哈希的例子如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.commons.codec.digest.DigestUtils;

public class ConsistentHashingTest {

// 真实缓存地址
private final String[] cacheServers = { "192.168.56.101:11211", "192.168.56.102:11211", "192.168.56.103:11211" };

// 保存虚拟节点
private final SortedMap<Long, String> nodes = new TreeMap<Long, String>();

// 每个虚拟节点的数量
private final int VIRTUAL_NODE_NUM = 3;

public ConsistentHashingTest(){
//初始化
for(String eachServer : cacheServers) {
this.addNode(eachServer);
}
}

// 创建虚拟节点
public void addNode(String nodeKey) {
//为每一个实体节点创建3个虚拟节点
for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
long eachHashCode = this.hash(nodeKey + ":" + i);
nodes.put(eachHashCode, nodeKey);
}
}

// hash 函数 可以使用 md5, sha-1, sha-256 等
// 虽然 md5, sha-1 哈希算法在签名领域已经不再安全,但运算速度比较快,在非安全领域是可以使用的。
// DigestUtils 是来自于 apache 中的 org.apache.commons.codec.digest 中的工具类
private long hash(String key) {
Stringmd5key = DigestUtils.md5Hex(key);
return Long.parseLong(md5key.substring(0, 15), 16) % ((long) Math.pow(2, 32));
}

//按照同一个方向寻找
public String getRealServer(String key) {
long hashCode = this.hash(key);
SortedMap<Long,String> tailMap =
nodes.tailMap(hashCode);
long serverKey = tailMap.isEmpty() ? nodes.firstKey() : tailMap.firstKey();
return nodes.get(serverKey);
}

public static void main(String[] args) {
ConsistentHashingTestt = new ConsistentHashingTest();
System.out.println(t.getRealServer("my-cache-key"));
}
}

另外,前文提到的(TODO 章节)Guava Cache 框架支持一致性哈希,实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
//实体缓存服务器
String[]cacheServers = { "192.168.56.101:11211", "192.168.56.102:11211", "192.168.56.103:11211" };

// 缓存数据的key
String key = "my-test-cache-key";

// 计算缓存 key 对应的 hash 值,这里使用 MurmurHash 算法,MurmurHash 是一种高性能低碰撞的算法。此外,还支持 md5、sha1/sha256/sha512、orc32、adler32 等哈希算法。
HashCode hashCode = Hashing.murmur3_32().newHasher().putString(key, Charsets.UTF_8).hash();

// 通过一致性哈希方式计算,缓存key对应的服务器主机是那一台,bucket 的范围在 0 ~ cacheServers.length -1
int bucket = Hashing.consistentHash(hashCode, cacheServers.length);

image

多路复用的几种方式以及区别?

  • select的优缺点
    • 优点
      • select的可移植性好,在某些unix下不支持poll。
      • select对超时值提供了很好的精度,精确到微秒,而poll式毫秒。
    • 缺点
      • 单个进程可监视的fd数量被限制,默认是1024。
      • 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
      • 对fd进行扫描时是线性扫描,fd剧增后,IO效率降低,每次调用都对fd进行线性扫描遍历,随着fd的增加会造成遍历速度慢的问题。
      • select函数超时参数在返回时也是未定义的,考虑到可移植性,每次超时之后进入下一个select之前都要重新设置超时参数。
  • poll的优缺点
    • 优点
      • 不要求计算最大文件描述符+1的大小。
      • 应付大数量的文件描述符时比select要快。
      • 没有最大连接数的限制是基于链表存储的。
    • 缺点
      • 大量的fd数组被整体复制于内核态和用户态之间,而不管这样的复制是不是有意义。
      • 同select相同的是调用结束后需要轮询来获取就绪描述符。
  • epoll的优缺点(epoll详解)

    • 支持一个进程打开大数目的socket描述符(FD)

      select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

    • IO效率不随FD数目增加而线性下降

      传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行 操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

    • epoll工作的两种模式

      EPOLL事件分发系统可以运转在两种模式下:边缘触发Edge Triggered (ET)、水平触发Level Triggered (LT)。
      LT 是缺省的工作方式:同时支持block和no-block socket;在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
      ET是高速工作方式:它只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述 符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知。
      对于LT模式epoll_wait清空就绪链表之后会检查该文件描述符是哪一种模式,如果为LT模式,且必须该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表,epoll_wait马上返回。而ET模式不会检查,只会调用一次,只通知就绪通知一次 。

    • epoll函数底层实现过程
      首先epoll_create创建一个epoll文件描述符,底层同时创建一个红黑树,和一个就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl将会添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。对于LT模式epoll_ctl清空就绪链表之后会检查该文件描述符是哪一种模式,如果为LT模式,且必须该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表,epoll_wait马上返回。ET模式不会检查,只会调用一次