[你真的知道如何使用 ES 吗?
前言
ES的功能真的非常强大,其背后有很多默认值,或者默认操作。这些默认操作优劣并存,优势在于我们可以迅速上手使用ES,劣势在于,其实这些默认值的背后涉及到很多底层原理,怎么做更合适,只有数据使用者知道。用ES的话来说,你比ES更懂你的数据,一些配置信息,限制信息,还是需要在了解了ES的功能之后进行人工限制。
你是否遇到:在使用了一段时间ES之后,你期望使用ES的其他功能,但因为字段类型而受限的问题?所有针对字段类型的修改都需要reindex。
在遇到一些问题后,我发现用ES很简单,但是会用ES很难。这让我下定决心一定好好了解ES,也就出现了本系列文章
前期会对ES的基本概念,常用功能的底层原理进行介绍,在对ES有了一定的了解之后,再来讨论ES到底适合应用在什么场景之下。
题主的理解若有偏差,欢迎指正????
ES(全称 Elastic Search)是一款开源的、近实时、高性能的分布式搜索引擎, 在DBRaking热门测评中,在搜索引擎类,ES在近3年的统计数据中都霸居榜首,可见的其深受大家的喜爱。
随着ES的功能越来越强大,其和数据库的边界也越来越小,除了进行全文检索,ES也支持聚合/排序。ES底层基于Lucene开发,针对Lucene的局限性,ES提供了RESTful API风格的接口、支持分布式、可水平扩展,同时它可以被多种编程语言调用。
ES很多基础概念以及底层实现其本质是Lucene的概念。
ps:本文所有的dsl查询、结果展示均基于ES v7.7
历史背景
Lucene的历史背景
下图这个人叫Doug Cutting,他是Hadoop语言和Lucene工具包的创始人。Doug Cutting毕业于斯坦福大学,在Xerox积累了一定的工作经验后,从1997年开始,利用业余时间开发出了Lucene。Lucene面世于1999年,并于2005年成为Apache*开源项目。
Lucene的特点:
- Lucene是基于java编写的,开源的全文检索引擎工具包。
- Lucene具有高性能:在相同的硬件环境下,基于 Hadoop 的 webmap(Lucene的第一个应用) 的反应速度是之前系统的 33 倍。
Lucene的局限性:
- 仅限于java开发。
- 类库的接口学习成本高:本质上Lucene就是一个编程库,可以按原始接口来调用,但是如果在应用程序中直接使用Lucene,需要覆盖大量的集成框架工作。
- 原生并不支持水平扩展,若需实现海量数据的搜索引擎,需在此基础上格外开发以支持分布式。
ES的历史背景
Shay Banon是ElasticSearch的创始人。2004年,Shay Banon基于Lucene开发了Compass,在考虑Compass的第三个版本时,他意识到有必要重写Compass的大部分内容,以“创建一个可扩展的搜索解决方案”。因此,他创建了“一个从头构建的分布式解决方案”,并使用了一个公共接口,即HTTP上的JSON,它也适用于Java以外的编程语言。
2010年,Shay Banon在发布了Elasticsearch的第一个版本。
在使用ES之前,一定要先了解ES的版本历史,这里只是列出来,可以在了解了基本概念之后再看。
ES多个版本可能出现破坏性变更,例如,在6.x,ES不允许一个Index中出现多个Type。在ES的官网,每个版本都对应着一个使用文档,下面列出一些比较重大的更新版本:
- 初始版本 0.7.0 2010年5月14日
- Zen Discovery 自动发现模块
- Groovy Client支持
- 简单的插件管理机制
- 更好支持ICU分词器
- 1.0.0 2014年2月14日
- 支持聚合分析Aggregations
- CAT API 支持
- 支持联盟查询
- 断路器支持
- Doc values 引入
- 2.0.0 2015年10月28日 - 增加了 pipleline Aggregations - query/filter 查询合并,都合并到query中,根据不同的context执行不同的查询 在 ES 中,有 Query 和 Filter 两种 Context - Query Context :相关性算分 - Filter Context :不需要算分(YES OR NO), 可以利用 Cache 获得更好的性能 - 存储压缩可配置 - Rivers 模块被移除 - Multicast 组播发现成为组件
- 5.0.0 2016年10月26日
- Lucene 6.x 的支持,磁盘空间少一半;索引时间少一半;查询性能提升25%;支持IPV6。
- Internal engine级别移除了用于避免同一文档并发更新的竞争锁,带来15%-20%的性能提升
- Shrink API ,它可将分片数进行收缩成它的因数,如之前你是15个分片,你可以收缩成5个或者3个又或者1个,那么我们就可以想象成这样一种场景,在写入压力非常大的收集阶段,设置足够多的索引,充分利用shard的并行写能力,索引写完之后收缩成更少的shard,提高查询性能
- 提供了 Painless 脚本,代替Groovy脚本
- 新增 Sliced Scroll类型,现在Scroll接口可以并发来进行数据遍历了。每个Scroll请求,可以分成多个Slice请求,可以理解为切片,各Slice独立并行,利用Scroll重建或者遍历要快很多倍。
- 引入新的字段类型 Text/Keyword 来替换 String
- 限制索引请求大小,避免大量并发请求压垮 ES
- 限制单个请求的 shards 数量,默认 1000 个
- 6.0.0 2017年8月31日
- Index sorting,即索引阶段的排序。
- 顺序号的支持,每个 es 的操作都有一个顺序编号(类似增量设计)
- 无缝滚动升级
- 逐步废弃type,在 6.0 里面,开始不支持一个 index 里面存在多个 type
- Index-template inheritance,索引版本的继承,目前索引模板是所有匹配的都会合并,这样会造成索引模板有一些冲突问题, 6.0 将会只匹配一个,索引创建时也会进行验证
- Load aware shard routing, 基于负载的请求路由,目前的搜索请求是全节点轮询,那么性能最慢的节点往往会造成整体的延迟增加,新的实现方式将基于队列的耗费时间自动调节队列长度,负载高的节点的队列长度将减少,让其他节点分摊更多的压力,搜索和索引都将基于这种机制。
- 已经关闭的索引将也支持 replica 的自动处理,确保数据可靠。
- 7.0.0 2019年4月10日
- 集群连接变化:TransportClient被废弃 以至于,es7的java代码,只能使用restclient
-
重大改进-正式废除单个索引下多Type的支持
- es6时,官方就提到了es7会删除type,并且es6时已经规定每一个index只能有一个type。在es7中使用默认的_doc作为type,官方说在8.x版本会彻底移除type。 api请求方式也发送变化,如获得某索引的某ID的文档:GET index/_doc/id其中index和id为具体的值
- Lucene9.0
- 引入了真正的内存断路器,它可以更精准地检测出无法处理的请求,并防止它们使单个节点不稳定
- Zen2 是 Elasticsearch 的全新集群协调层,提高了可靠性、性能和用户体验,变得更快、更安全,并更易于使用
- 性能优化 - Weak-AND算法提高查询性能 - 默认的Primary Shared数从5改为1,避免Over Sharding - shard也是一种资源,shard过多会影响集群的稳定性。因为shard过多,元信息会变多,这些元信息会占用堆内存。shard过多也会影响读写性能,因为每个读写请求都需要一个线程。 所以如果index没有很大的数据量,不需要设置很多shard。 - 更快的前 k 个查询
- 间隔查询(Intervals queries) 某些搜索用例(例如,法律和专利搜索)引入了查找单词或短语彼此相距一定距离的记录的需要。 Elasticsearch 7.0中的间隔查询引入了一种构建此类查询的全新方式,与之前的方法(跨度查询span queries)相比,使用和定义更加简单。 与跨度查询相比,间隔查询对边缘情况的适应性更强。
基础概念介绍
在开始介绍之前想碎碎念几句~ES的功能真的非常强大,本文仅讲述了题主接触过的部分;ES背后有很多默认值,或者默认操作,用ES的话来说,你比ES更懂你的数据,一些配置信息,限制信息,还是需要在了解了ES的功能之后进行人工限制。
index
Index翻译过来是索引的意思。在ES里,索引有两个含义:
- 名词:一个索引相当于关系型数据库中的一个表(在6.x以前,一个
index
可以被认为是一个数据库,可以承载相同scheme的数据。 - 动词:将一份
document
保存在一个index
里,这个过程也可以称为索引。
type
在6.x之前,index
可以被理解为关系型数据库中的【数据库】,而type
则可以被认为是【数据库中的表】,ES在8.x完全废弃了**type
**。
使用type
允许我们在一个index
里存储多种类型的数据,数据筛选时可以指定type
。type
的存在从某种程度上可以减少index
的数量,但是type
存在以下限制:
-
不同type里的字段需要保持一致。例如,一个
index
下的不同type
里有两个名字相同的字段,他们的类型(string, date 等等)和配置也必须相同。 - 只在某个
type
里存在的字段,在其他没有该字段的 type 中也会消耗资源。 - 得分是由
index
内的统计数据来决定的。也就是说,一个 type 中的文档会影响另一个 type 中的文档的得分。
以上限制要求我们,只有同一个index
的中的 type 都有类似的映射 (mapping) 时,才勉强适用 type
。否则,使用多个type
可能比使用多个index
消耗的资源更多。
这大概也是为什么ES决定废弃type这个概念,个人感觉type的存在,就像是一个语法糖,但是并未带来太大的收益,反而增加了复杂度。
document
index中的单条记录称为document
(文档),可以理解为表中的一行数据(本质差别在于document
的存储方式上,它并不是行存储)。
多条document
组成了一个index
。
"hits" : {
"total" : {
"value" : 140,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "cards_info_test",
"_type" : "_doc",
"_id" : "1092",
"_score" : 1.0,
"_source" : {
"title" : "秦时明月世界黑曜卡5000元"
...
}
}
]
}
上图为ES一条文档数据,其中:
_index
: 文档所属索引名称。
_type
:文档所属类型名(此处已默认为_doc)。
_id
:Doc的主键。在写入的时候,可以指定该Doc的ID值,如果不指定,则系统自动生成一个唯一的UUID值。
_score
:顾名思义,得分,也可称之为相关性,在查询是ES会 根据一些规则计算得分,并根据得分进行倒排。除此之外,ES支持通过Function score query
在查询时自定义score的计算规则。
_source
: 文档的原始JSON数据。
filed
一个document
会由一个或多个filed组成,filed是ES中数据索引的最小定义单位,下面仅列举部分常用的类型。
在ES中,没有数组类型,任何字段都可以变成数组。
string
text
- 索引全文值的字段,例如电子邮件正文或产品描述。
- 如果您需要索引结构化内容,例如电子邮件地址、主机名、状态代码或标签,您可能应该使用
keyword
字段。 - 出于不同目的,我们期望以不同方式索引同一字段,这就是 multi-fields 。
例如,可以将string
字段映射为用于全文搜索的text
字段,并映射为用于排序或聚合的keyword
字段:
PUT my_index
{
"mappings": {
"properties": {
"city": {
"type": "text",
"fields": {
"raw": {
"type": "keyword"
}
}
}
}
}
}
- ⚠️纯**
text
**字段默认无法进行排序或聚合 - ⚠️只用test字段一定要使用合理的分词器,scr上的ES已经默认只是了ik中文分词。
keyword
- 用于索引结构化内容的字段,例如 ID、电子邮件地址、主机名、状态代码、邮政编码或标签。
如果您需要索引全文内容,例如电子邮件正文或产品描述,你应该使用text
字段。
- 它们通常用于过滤(查找所有发布状态的博客文章)、排序和聚合。
keyword
字段只能精确匹配。 - 如果你能预知你的数据一定不会进行排序和聚合,那么在创建映射关系时,可以指定
"doc_values": false
。
doc_values是为了实现排序和聚合引入的,列式存储的数据格式,指定为false可以一定程度上节约空间。
Numeric
long, integer, short, byte, double, float, half_float, scaled_float...
- 就整数类型(
byte
、short
、integer
和long
)而言,应该选择足以满足用例的最小类型。 - 对于浮点类型,使用缩放因子将浮点数据存储到整数中通常更有效,这就是
scaled_float
类型的实现。 - 下面这个case,
scaling_factor
缩放因子设置为100,对于所有的API来说,price
看起来都像是一个双精度浮点数。但是对于ES内部,他其实是一个整数long
。
"price": {
"type": "scaled_float",
"scaling_factor": 100
}
- 如果
scaled_float
无法满足精度要求,可以使用double
、float
、half_float
。
Type |
Minimum value |
Maximum value |
Significant bits / digits |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
- 不是所有的字段都适合存储为
numberic
,numberic
********类型更擅长********range
**************类查询**,精确查询可以尝试使用keyword
。
mapping
mapping
是一个定义document
结构的过程,mapping
中定义了一个文档所包含的所有filed
信息。
定义字段索引过多会导致爆炸的映射,这可能会导致内存不足错误和难以恢复的情况,mapping
提供了一些配置对filed
进行限制,下面列举几个可能会比较常见的:
index.mapping.total_fields.limit
限制mapping
中filed的最大数量,默认值是1000(filed和object内的所有字段,都会加入计数)。
index.mapping.depth.limit
限制mapping
中object的最大深度,默认值是20。
index.mapping.field_name_length.limit
限制mapping
中字段名的长度,默认是没有限制。
dynamic mapping
在索引 document时,ES的动态mapping
会将新增内容中不存在的字段,自动的加入到映射关系中。ES会自动检测新增字段的逻辑,并赋予其默认值。
- One of the most important features of Elasticsearch is that it tries to get out of your way and let you start exploring your data as quickly as possible.
- You know more about your data than Elasticsearch can guess, so while dynamic mapping can be useful to get started, at some point you will want to specify your own explicit mappings.
截取了部分ES官方文档中的话术,ES认为一些自动化的操作会让新手上手更容易。但是同时,又提出,你肯定比ES更了解你的数据,可能刚开始使用起来觉得比较方便,但是最好还是自己明确定义映射关系。(????️个人认为,这些自动操作是在用户对ES没有太多了解的情况下进行的,如果刚开始依赖了这些默认的操作,例如:新增字段使用了ES赋予的默认值,如果后续有分析、排序、聚合等操作可能会有一定限制)
⚠️在ES中,删除/变更filed定义,需要进行reindex
,所以在构建mapping
结构时记得评估好字段的用途,以使用最合适的字段类型。
部分查询关键字介绍
match&&match_phrase
-
match
:用于执行全文查询的标准查询,包括模糊匹配和短语或接近查询。
重要参数:控制Token之间的布尔关系:operator:or/and
-
match_phrase
:与match查询类似
但用于匹配确切的短语或单词接近匹配。重要参数:Token之间的位置距离:slop 参数,默认为0
GET /_analyze
{
"text": ["这是测试"],
"analyzer": "ik_smart"
}
//Result
{
"tokens" : [
{
"token" : "这是",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "测试",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
}
]
}
//match+analyzer:ik_smart
//可以查询到所有describe中包含【这是测试】、【这是】、【测试】的doc
GET /my_index/_search
{
"query": {
"match": {
"describe":{
"query": "这是测试",
"analyzer": "ik_smart"
}
}
}
}
//match_phrase + analyzer:ik_smart + slop=0(默认)
//可以查询到所有describe中包含【这是】+【测试】token间隔为0的doc(说人话就是:模糊匹配【这是测试】)
GET /my_index/_search
{
"_source": "describe",
"query": {
"match_phrase": {
"describe": "这是测试"
}
}
}
//match_phrase + analyzer:ik_smart + slop=1
//可以查询到所有describe中包含【这是】+【测试】token间隔为1的doc
//例如某个doc中describe为【这是一个测试】,【这是一个测试】分词后的token分别为【这是】【一个】【测试】
//【这是】和【测试】之间间隔了1个token【一个】,所以可以被查询到;同理【这是一个我的测试】查询不到
GET /my_index/_search
{
"query": {
"match_phrase": {
"describe":{
"query": "这是测试",
"analyzer": "ik_smart",
"slop": 1
}
}
}
}
term
term
是进行精确查找的关键;在Lucene中,term是中索引和搜索的最小单位。一个filed会由一个或多个term
组成,term
是由filed经过Analyzer(分词)产生。Term Dictionary
即term
词典,是根据条件查找term
的基本索引。
- 避免对
text
字段使用术语查询。 默认情况下,ES 会在分析过程中更改文本字段的值。
这会使查找text
字段值的精确匹配变得困难。 要搜索text
字段值,强烈建议改用match
查询。
- ⚠️默认分词情况下,无论是
term
还是match
,都无法判断string
类型字段是否为空字符串
以上两点均是因为text
字段存储的是分词结果,如果字段值为空,分词结果将不会存储term
信息,keyword
字段存储的是原始内容。
GET /my_index/_termvectors/1657216298432499712?fields=content
{
"_index" : "my_index",
"_type" : "_doc",
"_id" : "1657216298432499712",
"_version" : 2,
"found" : true,
"took" : 0,
"term_vectors" : { }
}
GET /my_index/_termvectors/1672819694014398464?fields=card_pic
{
"_index" : "my_index",
"_type" : "_doc",
"_id" : "1672819694014398464",
"_version" : 1,
"found" : true,
"took" : 0,
"term_vectors" : {
"card_pic" : {
"field_statistics" : {
"sum_doc_freq" : 183252,
"doc_count" : 183252,
"sum_ttf" : 183252
},
"terms" : {
"" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 0
}
]
}
}
}
}
}
参考
https://www.jianshu.com/p/1a737a3dde86
https://www.modb.pro/db/130339
https://www.cnblogs.com/qdhxhz/p/11448451.html
https://blog.****.net/tengxing007/article/details/100663530
https://www.elastic.co/guide/en/elasticsearch/reference/7.7/index.html
https://zhuanlan.zhihu.com/p/35469104
https://zhuanlan.zhihu.com/p/142641300
推荐阅读
-
你知道如何在 PPT 中使用图像占位符吗?超级有用,技能不错
-
EXCEL 的 [SLOPE] 功能,你知道如何使用吗?
-
你真的需要 MacBook 吗?如何在 Windows 和 Mac OS 之间做出选择?
-
[你真的知道如何使用 ES 吗?
-
手把手教你如何注册谷歌账号--这个手机号不能用来验证。于是人们想当然地认为,谷歌退出的原因,并不是对国内手机号验证通过。其实不然,根据谷歌的注册机制,这是注册时的第一次验证,也属于机器验证,即判断注册者是否为机器人,以防止滥用,所以这次验证只是纯粹的手机号验证,并不是绑定手机号。 那么我们就有疑问了,第一次验证要求手机号验证却显示手机号无法验证,这不是自相矛盾吗? 谷歌现在在全球至少有14亿注册用户,所以谷歌对注册的网络、机器要求非常严格,可以说是判断筛选机制,你的网络、机器或者浏览器身份验证,不符合谷歌机器认证,就不允许你验证通过,而不是因为手机号的问题。不信的朋友可以换个美国手机号输入试试,同样显示这个手机号无法验证。 所以我不建议使用代理网络注册,因为那是一个共享通道,已经被滥用了,这个时候遇到无法验证也就不奇怪了,即使有些朋友侥幸通过了验证,这个号码也活不了多久,因为很快就会被检测出是什么ip注册的,然后出现异常验证,甚至直接封号,这个时候你就后悔晚矣。所以对于QQ邮箱谷歌注册入口,笔者强烈不推荐这样做,因为QQ邮箱注册早就被滥用了,会有那么多人会遇到异常验证的情况。 异常验证在你输入任何手机号码的情况下都会显示无法验证或者手机号码验证次数过多。
-
你真的知道如何使用 XMLHttpRequest 吗?
-
你真的知道如何使用 XMLHttpRequest 吗?
-
Java 中的 BigDecimal,您真的知道如何使用它吗?
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET
-
死鬼,你真的知道如何编写单元测试吗?