首页 文章 精选 留言 我的

精选列表

搜索[rest],共4247篇文章
优秀的个人博客,低调大师

使用腾讯会议 REST API 管理会议 Rooms

本文主要介绍企业版/商业版账号如何在会议预定/修改/释放 Rooms 设备,整体流程如下: ![room_all.jpg](https://developer.qcloudimg.com/http-save/yehe-12706950/933f7ff37a002e23478c47334e565417.jpg) ## 一、前提条件 1. **已开通腾讯会议企业版/商业版账号**,并在企业内完成会议室(Rooms)设备的激活与绑定。Rooms 设备状态可通过[查询设备列表](https://cloud.tencent.com/document/product/1095/55312)接口确认(`meeting_room_status` 为 2 表示空闲可用)。 2. **已创建企业自建应用**,获取 `appId`、 `sdkId`、`secretId` 和 `secretKey`。Rooms 相关接口目前暂不支持 OAuth 2.0 鉴权,必须使用 [AK/SK 签名验证方式](https://cloud.tencent.com/document/product/1095/42413);操作者需具备超级管理员/管理员角色或为会议创建者本人。 3. **会议已预约成功**:本文流程从"会议已创建"开始。会议可通过[创建会议](https://cloud.tencent.com/document/product/1095/42417)接口创建,成功后获得两个关键标识: - `meeting_id`:会议唯一 ID(后续所有 Rooms 操作的路径参数); - `meeting_code`:9 位会议号(用户侧可见)。 4. **会议本身满足 Rooms 预定约束**: - 会议时长不超过 24 小时 - 会议类型为普通预约会议,不支持周期性会议 - 会议未开始、未结束、未取消 ## 二、预定会议室 Rooms 对成功预定的会议添加会议室,支持一次预定多个会议室。 | 项目 | 内容 | | ------ | ------ | | 官方文档 | [预定会议室](https://cloud.tencent.com/document/product/1095/55313) | | 接口地址 | `https://api.meeting.qq.com/v1/meetings/{meeting_id}/book-rooms` | | 请求方式 | POST | | 操作者权限 | 会议创建者 | ### 2.1 请求参数 | 参数 | 必选 | 类型 | 说明 | | ------ | ------ | ------ | ------ | | meeting_id | 是 | String | 会议 ID | | meeting_room_id_list | 是 | String 数组 | 会议室 ID 列表,可先通过「查询设备列表」接口获取 | | operator_id | 是 | String | 操作者 ID,与 operator_id_type 配合使用 | | operator_id_type | 是 | Integer | 操作者 ID 类型,1 表示 userid | | subject_visible | 否 | Boolean | 会议开始前一小时是否在 Room 上显示会议主题,默认 true | > **注意**:`meeting_room_id`(会议室 ID)与 `rooms_id`(Rooms 设备 ID)是两个不同维度的标识,请勿混淆。 > > - `meeting_room_id`:会议室 ID,标识“会议室”这一业务资源,是本文预定/释放会议室所用的 ID(对应 `meeting_room_id_list` 参数)。 > - `rooms_id`:Rooms 设备 ID,标识会议室中部署/绑定的 Rooms 设备(终端),一个会议室下可绑定一个 Rooms 设备。 > > 预定会议室时请使用 `meeting_room_id`,可通过「查询会议室(Rooms)列表」或「查询设备列表」接口获取。 ### 2.2 请求与响应示例 请求示例: ```http POST https://api.meeting.qq.com/v1/meetings/6524541286408448530/book-rooms Content-Type: application/json { "operator_id": "KM4Ss4T******1JiK", "operator_id_type": 1, "meeting_room_id_list": ["100001", "100002"] } ``` 响应示例: ```json { "meeting_room_number": 2, "meeting_room_list": [ { "meeting_room_id": "100001", "meeting_room_name": "会议室测试1", "meeting_room_location": "广州-大厦-10" }, { "meeting_room_id": "100002", "meeting_room_name": "会议室测试2", "meeting_room_location": "西安-广场-19" } ] } ``` > **注意**:预定会议室为**覆盖型接口**,每次调用都会以本次传入的 `meeting_room_id_list` 覆盖该会议此前已预定的会议室列表。若需在原有基础上追加会议室,请传入「原有 + 新增」的完整列表。 ## 三、查询会议室预定情况 预定或变更 Rooms 前,建议先确认两件事:**会议室是否开放预定**、**目标时段是否已被占用**。这两项分别对应下面两个查询接口。 ### 3.1 查询会议室(Rooms)列表 用于获取企业下的会议室 ID 及其可预定状态,是获取 `meeting_room_id` 的推荐入口。 | 项目 | 内容 | | ------ | ------ | | 官方文档 | [查询会议室(Rooms)列表](https://cloud.tencent.com/document/product/1095/55309) | | 接口地址 | `https://api.meeting.qq.com/v1/meeting-rooms` | | 请求方式 | GET | | 操作者权限 | 会议创建者 | #### 3.1.1 请求参数 | 参数 | 必选 | 类型 | 说明 | | ------ | ------ | ------ | ------ | | operator_id | 是 | String | 操作者 ID,与 operator_id_type 配合使用 | | operator_id_type | 是 | Integer | 操作者 ID 类型,1 表示 userid | | page | 否 | Integer | 页码,从 1 开始,默认 1 | | page_size | 否 | Integer | 分页大小,最大 50,默认 20 | | meeting_room_name | 否 | String | 会议室名称,支持模糊匹配 | #### 3.1.2 请求与响应示例 请求示例: ```http GET https://api.meeting.qq.com/v1/meeting-rooms?page=1&page_size=2&meeting_room_name=会议室&operator_id=KM4Ss4T******1JiK&operator_id_type=1 ``` 响应示例: ```json { "total_count": 200, "total_page": 100, "current_page": 1, "current_size": 2, "meeting_room_list": [ { "meeting_room_id": "100001", "meeting_room_name": "会议室测试1", "meeting_room_location": "广州-大厦-10", "account_type": 1, "active_code": "69MS********2PKC", "participant_number": 12, "meeting_room_status": 2, "scheduled_status": 1, "is_allow_call": true, "room_type": 0 } ] } ``` #### 3.1.3 关键字段说明 预定前需重点关注以下两个字段,二者共同决定该会议室能否被预定: | 字段 | 取值 | 说明 | | ------ | ------ | ------ | | scheduled_status | 0:未开放预定;1:开放预定 | 为 0 时调用「预定会议室」会返回错误码 20003(会议室未开放预定) | | meeting_room_status | 0:未激活;1:未绑定;2:空闲;3:使用中;4:离线 | 设备实时状态,2 表示空闲可用 | | room_type | 0:Rooms;1、2:MRA | 用于区分 Rooms 设备与 MRA 设备 | ### 3.2 查询会议室(Rooms)下的会议列表 用于查询某个会议室在指定时间段内已被预定的会议,是判断目标时段是否被占用的依据;变更会议时间前应先调用该接口,避免因会议室被占用导致修改失败。 | 项目 | 内容 | | ------ | ------ | | 官方文档 | [查询会议室(Rooms)下的会议列表](https://cloud.tencent.com/document/product/1095/67435) | | 接口地址 | `https://api.meeting.qq.com/v1/meeting-rooms/{operator_id}/meetings` | | 请求方式 | GET | | 操作者权限 | 会议创建者 | #### 3.2.1 请求参数 | 参数 | 必选 | 类型 | 说明 | | ------ | ------ | ------ | ------ | | operator_id | 是 | String | 操作者 ID,与 operator_id_type 配合使用 | | operator_id_type | 是 | Integer | 操作者 ID 类型,1 表示 userid | | instanceid | 是 | Integer | 终端设备类型,服务端调用可填 1(PC)或 5(Web) | | target_rooms_id | 否 | Integer | 目标查询的会议室 ID 或 Rooms 设备 ID,具体类型由 target_rooms_id_type 决定 | | target_rooms_id_type | 否 | Integer | 3 表示 rooms 设备的 rooms_id;5 表示会议室 ID(meeting_room_id) | | start_time | 否 | String | 查询起始时间,Unix 时间戳(秒) | | end_time | 否 | String | 查询结束时间,Unix 时间戳(秒) | | page | 否 | Integer | 页码,从 1 开始,默认 1 | | page_size | 否 | Integer | 分页大小,最大 20,默认 20 | > `start_time` 与 `end_time` 的查询区间不超过 90 天。 #### 3.2.2 请求与响应示例 请求示例: ```http GET https://api.meeting.qq.com/v1/meeting-rooms/KM4Ss4T******1JiK/meetings?operator_id_type=1&instanceid=1&target_rooms_id=100001&target_rooms_id_type=5&start_time=1572172200&end_time=1572175800 ``` 响应示例: ```json { "current_page": 1, "current_size": 1, "total_count": 1, "total_page": 1, "meeting_info_list": [ { "meeting_id": "6524541286408448530", "meeting_code": "637228110", "subject": "周会", "status": "MEETING_STATE_INIT", "meeting_type": 0, "start_time": 1572172200, "end_time": 1572175800 } ] } ``` 若返回的 `meeting_info_list` 中存在与目标时段重叠、且 `status` 为 `MEETING_STATE_INIT`(待开始)或 `MEETING_STATE_STARTED`(已开始)的会议,说明该时段会议室已被占用,需改用其他会议室或调整时间。 ## 四、修改会议时间/更换会议室 会议已添加 Rooms 后,常见的变更诉求分为两类:修改会议时间、更换会议室 Rooms。两者的处理方式不同,需分别对待。 Rooms 的预定时间本身不可单独操作,它始终跟随所属会议的时间,因此时间变更需通过「修改会议」接口完成。 | 项目 | 内容 | | ------ | ------ | | 官方文档 | [修改会议](https://cloud.tencent.com/document/product/1095/42424) | | 接口地址 | `https://api.meeting.qq.com/v1/meetings/{meetingId}` | | 请求方式 | PUT | | 操作者权限 | 会议创建者、企业管理员 | ### 4.1 修改会议时间 修改会议时间的整体流程如下: ![会议时间修改流程图.png](https://developer.qcloudimg.com/http-save/yehe-12706950/78b6b06fbe6221ea42e870af69db62cd.png) #### 4.1.1 第一步:释放 Rooms 请求示例: ```http POST https://api.meeting.qq.com/v1/meetings/6524541286408448530/release-rooms Content-Type: application/json { "operator_id": "KM4Ss4T******1JiK", "operator_id_type": 1, "meeting_room_id_list": ["100001", "100002"] } ``` 成功返回空对象 `{}`。接口详细参数见 5.1。 #### 4.1.2 第二步:修改会议时间 请求参数(时间修改相关): | 参数 | 必选 | 类型 | 说明 | | ------ | ------ | ------ | ------ | | meetingId | 是 | String | 会议 ID,路径参数 | | userid | 是 | String | 会议创建者或企业管理员 ID(OAuth 2.0 场景为 openId) | | instanceid | 是 | Integer | 终端设备类型,服务端调用可填 1 | | start_time | 否 | String | 会议开始时间戳(秒),需大于当前时间 | | end_time | 否 | String | 会议结束时间戳(秒),需大于开始时间 | | subject | 否 | String | 会议主题,不修改时可不传 | 请求示例: ```http PUT https://api.meeting.qq.com/v1/meetings/6524541286408448530 Content-Type: application/json { "userid": "zhangsan", "instanceid": 1, "subject": "周会", "start_time": "1572172200", "end_time": "1572175800" } ``` 响应示例: ```json { "meeting_number": 1, "meeting_info_list": [ { "meeting_id": "6524541286408448530", "meeting_code": "637228110" } ] } ``` > **注意**:修改会议时间后仍需满足 Rooms 的预定约束,即新的会议时长不得超过 24 小时,且会议未开始、未结束、未取消。 #### 4.1.3 第三、四步:确认空闲并重新预定 确认新时段会议室空闲后,调用「预定会议室」重新预定: 请求示例: ```http POST https://api.meeting.qq.com/v1/meetings/6524541286408448530/book-rooms Content-Type: application/json { "operator_id": "KM4Ss4T******1JiK", "operator_id_type": 1, "meeting_room_id_list": ["100001", "100002"] } ``` ### 4.2 更换会议室 Rooms 「预定会议室」是**覆盖型接口**,因此在会议时间不变的前提下更换会议室,不需要先释放,直接传入更换后的完整会议室列表即可。 以「将会议室 100001、100002 更换为 100003」为例: 请求示例: ```http POST https://api.meeting.qq.com/v1/meetings/6524541286408448530/book-rooms Content-Type: application/json { "operator_id": "KM4Ss4T******1JiK", "operator_id_type": 1, "meeting_room_id_list": ["100003"] } ``` 响应示例: ```json { "meeting_room_number": 1, "meeting_room_list": [ { "meeting_room_id": "100003", "meeting_room_name": "会议室测试3", "meeting_room_location": "深圳-大厦-8" } ] } ``` 调用成功后,该会议仅保留 100003,原 100001、100002 自动释放。 ## 五、取消会议和 Rooms 当会议需要取消时,需严格按照先释放会议室、再取消会议的顺序执行。若先取消会议,「释放会议室」接口会因会议已取消而调用失败,导致会议室资源无法通过接口释放。 ### 5.1 第一步:释放会议室 | 项目 | 内容 | | ------ | ------ | | 官方文档 | [释放会议室(Rooms)](https://cloud.tencent.com/document/product/1095/55314) | | 接口地址 | `https://api.meeting.qq.com/v1/meetings/{meeting_id}/release-rooms` | | 请求方式 | POST | | 操作者权限 | 会议创建者 | #### 5.1.1 请求参数 | 参数 | 必选 | 类型 | 说明 | | ------ | ------ | ------ | ------ | | meeting_id | 是 | String | 会议 ID,路径参数 | | meeting_room_id_list | 是 | String 数组 | 待释放的会议室 ID 列表 | | operator_id | 是 | String | 操作者 ID,与 operator_id_type 配合使用 | | operator_id_type | 是 | Integer | 操作者 ID 类型,1 表示 userid | #### 5.1.2 请求与响应示例 请求示例: ```http POST https://api.meeting.qq.com/v1/meetings/6524541286408448530/release-rooms Content-Type: application/json { "operator_id": "KM4Ss4T******1JiK", "operator_id_type": 1, "meeting_room_id_list": ["100001", "100002"] } ``` 响应示例: ```json {} ``` 支持一次释放多个会议室。 > **注意**:「查询会议」接口的响应中不包含会议室(Rooms)字段,无法直接查到某会议已预定哪些会议室。建议在调用「预定会议室」时将响应中的 `meeting_room_list` 记录在业务侧,作为后续释放的依据。 ### 5.2 第二步:取消会议 | 项目 | 内容 | | ------ | ------ | | 官方文档 | [取消会议](https://cloud.tencent.com/document/product/1095/42422) | | 接口地址 | `https://api.meeting.qq.com/v1/meetings/{meetingId}/cancel` | | 请求方式 | POST | | 操作者权限 | 会议创建者 | #### 5.2.1 请求参数 | 参数 | 必选 | 类型 | 说明 | | ------ | ------ | ------ | ------ | | meetingId | 是 | String | 会议 ID,路径参数 | | userid | 是 | String | 调用方用户 ID(OAuth 2.0 场景为 openId) | | instanceid | 是 | Integer | 终端设备类型 | | reason_code | 是 | Integer | 取消原因代码,可自定义 | | reason_detail | 否 | String | 详细取消原因描述 | | meeting_type | 否 | Integer | 默认 0;周期性会议传 1 | | sub_meeting_id | 否 | String | 仅取消周期会议的单个子会议时传入;不传则取消整个系列 | #### 5.2.2 请求与响应示例 请求示例: ```http POST https://api.meeting.qq.com/v1/meetings/6524541286408448530/cancel Content-Type: application/json { "userid": "zhangsan", "instanceid": 1, "reason_code": 1, "reason_detail": "会议取消" } ``` 取消成功返回空消息体。

优秀的个人博客,低调大师

中文代码之Spring Boot实现简单REST服务

最终目标详见: 参考MSDN,试搞.NET类库标识符的翻译版 · Issue #54 · program-in-chinese/overview 此文仅为技术探索+原型搭建的第一小步. 源码库: 演示原型 · program-in-chinese/programming_term_dictionary@6bb00f3 主要部分源码如下:应用.java @SpringBootApplication public class 应用 { public static void main(String[] 参数) { SpringApplication.run(应用.class, 参数); } } 词典控制器.java @RestController public class 词典控制器 { private static final HashMap<String, String> 英中词典 = new HashMap<>(); static { 英中词典.put("List", "列表"); } private final AtomicLong 计数器 = new AtomicLong(); @GetMapping("/") @ResponseBody public 词条 取词条(@RequestParam(name = "term", required = false, defaultValue = "") String 英文术语) { if (英中词典.containsKey(英文术语)) { return new 词条(计数器.incrementAndGet(), 英中词典.get(英文术语)); } return null; } } 词条.java public class 词条 { // TODO: 仅为演示用 private final long id; private final String 中文术语; public 词条(long id, String 中文术语) { this.id = id; this.中文术语 = 中文术语; } public long getId() { return id; } public String get中文术语() { return 中文术语; } } 在演示服务器上构建并从jar包启动服务: $ mvn package && java -jar target/programming-term-dictionary-0.1.0.jar --server.port=8090 在本地(客户端)访问, 或直接在浏览器访问http://74.91.17.250:8090/?term=List: $ curl 74.91.17.250:8090?term=List {"id":3,"中文术语":"列表"} 参考文档: Building a RESTful Web Service with Spring Boot Actuator 2018-08-14

优秀的个人博客,低调大师

全链路异步Rest客户端 ESA RestClient

ESA Stack(Elastic Service Architecture) 是OPPO云计算中心孵化的技术品牌,致力于微服务相关技术栈,帮助用户快速构建高性能,高可用的云原生微服务。产品包含高性能Web服务框架、RPC框架、服务治理框架、注册中心、配置中心、调用链追踪系统,Service Mesh、Serverless等各类产品及研究方向。 当前部分产品已经对外开源: 开源主站: https://www.esastack.io Github: https://github.com/esastack RestClient 项目地址: https://github.com/esastack/esa-restclient RestClient 文档地址: https://www.esastack.io/esa-restclient ESA RestClient ESA RestClient 是一个基于 Netty 的全链路异步事件驱动的高性能轻量级的HTTP客户端。 以下简称RestClient 1、Quick Start Step1:添加依赖 <dependency> <groupId>io.esastack</groupId> <artifactId>restclient</artifactId> <version>1.0.0</version></dependency> Step2: 构建RestClient并发送请求处理响应 final RestClient client = RestClient.ofDefault(); //快速创建RestClient,各项配置均为默认配置。 //如果用户想自定义一些配置,则可以使用RestClient.create()来进行自定义配置。client.post("http://127.0.0.1:8081/") .entity("Hello Server") //设置请求体 .execute() //执行请求逻辑 .thenAccept((response)-> { //异步处理响应 try { System.out.println(response.bodyToEntity(String.class)); //调用response.bodyToEntity(Class TargetClass)来 Decode 响应, //TargetClass为期望的响应类型 } catch (Exception e) { e.printStackTrace(); } }); 2、功能特性 ·Http1/H2/H2cUpgrade/Https ·Encode 与 EncodeAdvice ·Decode 与 DecodeAdvice ·RestInterceptor ·大文件发送 ·请求级别读超时 · 请求级别重试 ·请求级别重定向 ·100-expect-continue ·Multipart ·Metrics ·more … 2.1 Encode 与 EncodeAdvice 2.1.1 Encode RestClient会自动根据用户的 Headers 与 Entity 等选择合适的Encoder进行Encode。其内置了下面这些Encoder: · Json jackson(默认) fastjson gson · ProtoBuf · File · String · byte[] 除此之外RestClient也支持用户自定义Encoder。 2.1.1.1 使用Json Encoder 指定contentType为 MediaType.APPLICATION_JSON,将自动使用Json Encoder来对Entity来进行Encode。示例如下: final RestClient client = RestCient.ofDefault();client.post("localhost:8080/path") .contentType(MediaTpe.APPLICATION_JSON) .entity(new Person("Bob","male")) .execute(); 2.1.1.2 使用ProtoBuf Encoder 指定contentType为ProtoBufCodec.PROTO_BUF,且Entity类型为com.google.protobuf.Message的子类时,将自动使用ProtoBuf Encoder来对Entity来进行Encode。示例如下: final RestClient client = RestClient.ofDefault();client.post("localhost:8080/path") .contentType(ProtoBufCodec.PROTO_BUF) .entity(message) .execute(); 2.1.1.3 使用File Encoder 当Entity类型为File时,将自动使用File Encoder来对Entity来进行Encode。示例如下: final RestClient client = RestClient.ofDefault();client.post("localhost:8080/path") .entity(new File("tem")) .execute(); 2.1.1.4 自定义Encoder 当RestClient内置的Encoder无法满足用户需求时,用户可以自定义Encoder,示例如下: public class StringEncoder implements ByteEncoder { @Override public RequestContent<byte[]> doEncode(EncodeContext<byte[]> ctx) { if (MediaType.TEXT_PLAIN.equals(ctx.contentType())) { if (ctx.entity() != null) { return RequestContent.of(((String) ctx.entity()).getBytes(StandardCharsets.UTF_8)); } else { return RequestContent.of("null"); } } //该Encoder无法Encode这种类型,将Encode工作交给下一个Encoder return ctx.next(); }} 用户可以将自定义的Encoder直接绑定到请求或者Client上,同时也支持用户通过SPI的方式加载Encoder,具体参见文档:《RestClient 配置Encoder》 2.1.1.5 Encode执行时机 见请求处理完整流程中的Encoder。 2.1.2 EncodeAdvice 用户可以通过EncodeAdvice在Encode前后插入业务逻辑,来对要Encode的 Entity 或者 Encode后的RequestContent 进行修改替换等操作。 2.1.2.1 示例 public class EncodeAdviceImpl implements EncodeAdvice { @Override public RequestContent<?> aroundEncode(EncodeAdviceContext ctx) throws Exception { //...before encode RequestContent<?> requestContent = ctx.next(); //...after encode return requestContent; }} 用户可以将自定义的EncodeAdvice直接绑定到Client上,同时也支持用户通过SPI的方式加载EncodeAdvice,具体参见文档:《RestClient 配置EncodeAdvice》 2.1.2.2 执行时机 见请求处理完整流程中的EncodeAdvice。 2.2 Decode 与 DecodeAdvice 2.2.1 Decode RestClient会自动根据用户的 Headers 与 期望Entity类型 等选择合适的Decoder进行Decode。RestClient内置了下面这些Decoder: · Json jackson(默认) fastjson gson · ProtoBuf · String · byte[] 除此之外RestClient也支持用户自定义解码器。 2.2.1.1 使用Json Decoder 当Response的contentType为 MediaType.APPLICATION_JSON,将自动使用Json Decoder来来进行Decode。 final RestClient client = RestClient.ofDefault();client.get("localhost:8080/path") .execute() .thenAccept((response)-> { try { //当 MediaType.APPLICATION_JSON.equals(response.contentType()) 时将自动使用Json Decoder System.out.println(response.bodyToEntity(Person.class)); } catch (Exception e) { e.printStackTrace(); } }); 2.2.1.2 使用ProtoBuf Decoder 当Response的contentType为 ProtoBufCodec.PROTO_BUF, 且response.bodyToEntity()传入的类型为com.google.protobuf.Message的子类时,将自动使用ProtoBuf Decoder来进行Decode。 final RestClient client = RestClient.ofDefault();client.get("localhost:8080/path") .execute() .thenAccept((response)-> { try { //当 ProtoBufCodec.PROTO_BUF.equals(response.contentType()),且 Person 为 Message 的子类时,将自动使用ProtoBuf Decoder System.out.println(response.bodyToEntity(Person.class)); } catch (Exception e) { e.printStackTrace(); } }); 2.2.1.3 自定义Decoder 当RestClient内置的Decoder无法满足用户需求时,用户可以自定义Decoder,示例如下: public class StringDecoder implements ByteDecoder { @Override public Object doDecode(DecodeContext<byte[]> ctx) { if (String.class.isAssignableFrom(ctx.targetType())) { return new String(ctx.content().value()); } return ctx.next(); }} 用户可以将自定义的Decoder直接绑定到请求或者Client上,同时也支持用户通过SPI的方式加载Decoder,具体参见文档:《RestClient 配置Decoder》 2.2.1.4 执行时机 见请求处理完整流程中的Decoder。 2.2.2 DecodeAdvice 用户可以通过DecodeAdvice在Decode前后进行来插入业务逻辑,来对要解码的 ResponseContent 或者 Decode后的对象 进行修改替换等操作。 2.2.2.1 示例 public class DecodeAdviceImpl implements DecodeAdvice { @Override public Object aroundDecode(DecodeAdviceContext ctx) throws Exception { //...before decode Object decoded = ctx.next(); //...after decode return decoded; }} 用户可以将自定义的DecodeAdvice直接绑定到Client上,同时也支持用户通过SPI的方式加载DecodeAdvice,具体参见文档:《RestClient 配置DecodeAdvice》 2.2.2.2 执行时机 见请求处理完整流程中的DecodeAdvice。 2.3 RestInterceptor 用户可以使用RestInterceptor在请求发送前和响应接收后来插入业务逻辑。RestClient支持通过builder配置和SPI加载两种方式配置RestInterceptor。 2.3.1 Builder配置 在构造RestClient时传入自定义的RestInterceptor实例,如: final RestClient client = RestClient.create() .addInterceptor((request, next) -> { System.out.println("Interceptor"); return next.proceed(request); }).build(); 2.3.2 SPI 2.3.2.1 普通SPI RestClient支持通过SPI的方式加载RestInterceptor接口的实现类,使用时只需要按照SPI的加载规则将自定义的RestInterceptor放入指定的目录下即可。 2.3.2.2 RestInterceptorFactory 如果用户自定义的RestInterceptor对于不同RestClient的配置有不同的实现,则用户可以实现RestInterceptorFactory接口,并按照SPI的加载规则将自定义的RestInterceptorFactory放入指定的目录下即可。 public interface RestInterceptorFactory { Collection<RestInterceptor> interceptors(RestClientOptions clientOptions);} 在RestClient构建时将调用 RestInterceptorFactory.interceptors(RestClientOptions clientOptions),该方法返回的所有RestInterceptor都将加入到构建好的RestClient中。 2.3.2.3 执行时机 见请求处理完整流程中的RestInterceptor。 2.4 大文件发送 当文件较小时,可通过直接将文件内容写入请求body来发送文件。但是当文件内容过大时,直接写入会有OOM风险。 为了解决这个问题,RestClient借助底层的Netty使用NIO以零拷贝的方式发送文件,避免了OOM的同时又减少了数据的多次拷贝。 用户只需要简单的接口调用便可使用该功能: final RestClient client = RestClient.ofDefault();final String entity = client.post("http://127.0.0.1:8081/") .entity(new File("bigFile")) .execute(); 2.5 读超时 RestClient支持请求级别的读超时,同时也支持Client 级别的读超时。默认读超时为6000L。 2.5.1 Client级别读超时 Client级别的读超时将对该Client下的所有请求生效,具体配置方式如下: final RestClient client = RestClient.create() .readTimeout(3000L) .build(); 2.5.2 Request级别读超时 当Request设置了读超时,其数据将覆盖Client设置的读超时,具体配置方式如下: final RestClient client = RestClient.ofDefault();client.get("http://127.0.0.1:8081/") .readTimeout(3000L) .execute() .thenAccept((response)-> { try { System.out.println(response.bodyToEntity(String.class)); } catch (Exception e) { e.printStackTrace(); } }); 2.6 重试 RestClient支持请求级别的重试,同时也支持Client 级别的重试。 默认情况下,RestClient仅会对所有抛出连接异常的请求进行重试(防止服务端的服务为非幂等),其中:最大重试次数为3(不包括原始请求),重试间隔时间为0。使用时,可以通过自定义RetryOptions参数更改重试次数、重试条件、重试间隔时间等。 2.6.1 Client级别重试 Client级别的重试将对该Client下的所有 Request 生效,使用时,可以通过自定义RetryOptions参数更改重试次数、重试条件、重试间隔时间等。具体配置方式如下 final RestClient client = RestClient.create() .retryOptions(RetryOptions.options() .maxRetries(3) //设置每次重试的间隔时间 .intervalMs(retryCount-> (retryCount+1) * 3000L) //判断是否要重试 .predicate((request, response, ctx, cause) -> cause != null) .build()) .connectionPoolSize(2048) .build(); 2.6.2 Request级别重试 当Request设置了重试次数,其数据将覆盖Client设置的重试次数,具体配置方式如下: final RestClient client = RestClient.ofDefault();client.get("http://127.0.0.1:8081/") .maxRetries(3) .execute() .thenAccept((response)-> { try { System.out.println(response.bodyToEntity(String.class)); } catch (Exception e) { e.printStackTrace(); } }); 2.7 重定向 默认情况下,RestClient会对响应状态码为301,302,303,307,308的请求重定向,其中:最大重定向次数为5(不包含原始请求)。使用时,可以通过maxRedirects更新重定向次数或者禁用(maxRedirects=0)重定向功能。 2.7.1 Client设置重定向 Client级别的重定向将对该Client下的所有 Request 生效,具体配置方式如下: final RestClient client = RestClient.create() .maxRedirects(3) .build(); 2.7.2 Request设置重定向覆盖Client的设置 当Request设置了重定向次数,其数据将覆盖Client设置的重定向次数,具体配置方式如下: final RestClient client = RestClient.ofDefault();client.get("http://127.0.0.1:8081/") .maxRedirects(3) .execute() .thenAccept((response)-> { try { System.out.println(response.bodyToEntity(String.class)); } catch (Exception e) { e.printStackTrace(); } }); 2.8 其它功能 如果用户想对RestClient的功能有进一步了解,可以参考:《RestClient 功能文档》。 3 性能表现 3.1 测试场景 服务端为一个Echo服务器,客户端分别使用RestClient、Apache HttpAsyncClient 、 OK Httpclient均使用POST请求,请求体内容为固定字符串: OK,响应体内容也为固定字符串:OK。 3.2 机器配置 客户端 OS 内存(G) CPU核数 Client CentOS Linux release 7.7.1908 (Core) 8 4 Server CentOS Linux release 7.7.1908 (Core) 16 8 3.3 JVM参数 -Xms1024m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 3.4 客户端版本 客户端 RestClient Apache HttpAsyncClient OK HttpClient 版本 1.0.0 5.1.2 4.9.2 3.5 测试方法 如何测试异步客户端的性能?这是我们在性能测试前要面对的第一个问题,下面是我们对于该问题的一些思考: 能否for循环发起同步请求,同时使用多线程来达到框架的请求处理极限,并将该极限视为客户端的最佳TPS? 一般来说,用户既然选择异步客户端,肯定大部分时间都会使用异步的方式去发起请求,使用同步方式进行测试的结果并不能代表异步时客户端的性能。因此对于异步客户端的用户而言,同步方式测试的最终结果并没有很大的参考价值。 因此这种方式并不适合用来对异步客户端进行性能测试。 能否使用单线程for循环异步发起请求,直接将这个时候的TPS视为客户端的最佳TPS? 异步客户端异步发起请求时,发起请求的方法返回的非常快(因为请求执行的过程主要在IO线程池中进行)。尽管只使用单线程,如果一直for循环异步发起请求,请求发起的速度也会比IO线程池处理请求的速度快得多,这会导致大量请求在程序中的某个地方(如获取连接)堆积,从而导致程序报错,或性能低下。 因此这种方式也并不适合用来对异步客户端进行性能测试。 那么应该如何测试异步客户端的性能呢? 异步客户端专为异步而生,用户既然选择异步客户端,肯定大部分时间都会使用异步的方式发起请求,因此对于异步客户端而言,使用异步的方式去测试其性能是一种更加合适的方式。 问题的关键在于在测试过程中,如何避免过快地发起异步请求导致发起请求的速度超过框架的处理能力。 主要问题确定了,那么答案基本也就确定了。要避免过快地发起异步请求,我们可以想办法调整异步请求发起的速度,对于调整异步请求的发起速度,我们可以尝试用以下两种方式: · for循环周期性地发送一定次数的异步请求后,sleep一会儿,然后再继续发起异步请求。我们可以通过控制sleep的时间和控制多少个请求间隔进行sleep 两个变量来控制异步请求的发起速率。 · for循环周期性地发送一定次数的异步请求后,发送一个同步请求,然后再继续发起异步请求。用同步请求去代替sleep的时间,该同步请求执行完恰好说明了请求队列中的请求都已经排队结束。其实原理是相同的,但这样控制的变量更少一些,仅需要控制发起多少个异步请求后发起一次同步请求(即:一个周期内异步请求次数与同步请求次数的比例)。 上面两种方法都可以控制异步请求的发起速率,最终我们选择使用第二种方法来控制异步请求的发起速率,因为第二种方式需要控制的变量更少,这样我们的测试过程也会更加简单。 因此最终我们的测试方法为: 使用异步与同步交替的方式来发起请求,不断调整一个周期内异步请求与同步请求的比例,在每个比例下调整客户端的各项配置,使其达到最佳的TPS,记录每个比例下,框架的最佳TPS,直到找到增加 异步请求与同步请求的比例 时,框架的TPS不再上升,甚至下降的拐点,该拐点即为框架的性能极限点。 3.6 测试结果 上图中,横坐标为异步请求与同步请求的比例,纵坐标为TPS,通过上图我们可以看出: RestClient:随着异步与同步请求比例增大而先增大后减小,异步与同步请求比例为800时,TPS最佳,为111217.98。 Apache HttpAsyncClient:随着异步与同步请求比例增大而先增大后减小,异步与同步请求比例为800时,TPS最佳,为 83962.54。 OK Httpclient :随着异步与同步请求比例增大而先增大后减小,异步与同步请求比例为300 时,TPS最佳,为 70501.59。 3.7 结论 RestClient在上面场景中最佳TPS 较 Apache HttpAsyncClient的最佳TPS高 32%,较OK Httpclient的最佳TPS高57% 。 4 架构设计 4.1 设计原则 · 高性能:持续不懈追求的目标 & 核心竞争力。 · 高扩展性:开放扩展点,满足业务多样化的需求。 · 全链路异步:基于CompletableStage提供完善的异步处理能力。 4.2 结构设计 上图为RestClient的结构图,我们由上到下依次介绍一下各个部分的含义: 4.2.1RestInterceptorChain RestInterceptorChain为RestInterceptor的集合,用户调用请求时,将依次经过RestInterceptorChain中的所有RestInterceptor。用户可以通过实现RestInterceptor中的getOrder()方法来指定其在RestInterceptorChain中的排序。 4.2.2EncodeAdviceChain EncodeAdviceChain为EncodeAdvice的集合,在Encode前,将依次经过EncodeAdviceChain中的所有EncodeAdvice。用户可以通过实现EncodeAdvice中的getOrder()方法来指定其在EncodeAdviceChain中的排序。 4.2.3EncoderChain EncoderChain为Encoder的集合,在Encode时,将依次经过EncoderChain中的所有Encoder,直到某个Encoder直接返回Encode的结果(即:其可以Encode该请求)。用户可以通过实现Encoder中的getOrder()方法来指定其在EncoderChain中的排序。 4.2.4DecodeAdviceChain DecodeAdviceChain为DecodeAdvice的集合,在Decode前,将依次经过DecodeAdviceChain中的所有DecodeAdvice。用户可以通过实现DecodeAdvice中的getOrder()方法来指定其在DecodeAdviceChain中的排序。 4.2.5DecoderChain DecoderChain为Decoder的集合,在Decode时,将依次经过DecoderChain中的所有Decoder,直到某个Decoder直接返回Decode的结果(即:其可以Decode该响应)。用户可以通过实现Decoder中的getOrder()方法来指定其在DecoderChain中的排序。 4.2.6NettyTransceiver NettyTransceiver 是 RestClient与其底层框架Neety之间连接的桥梁,在介绍其之前,需要一些预备知识,我们先来简单介绍一下这些预备知识: 4.2.6.1Channel&ChannelPool&ChannelPools Channel :Channel是Netty网络操作抽象类,它聚合了一组功能,包括但不限于网络的读、写,客户端发起连接、主动关闭连接,链路关闭,获得通信双方的网络地址等。它也包含了Netty框架相关的一些功能,包括获取该Channel的EventLoop,获取缓冲分配器ByteBufAllocator和pipeline等。 ChannelPool:ChannelPool用于缓存Channel,它允许获取和释放Channel,并充当这些Channel的池,从而达到复用Channel的目的。在RestClient中,每一个Server host对应一个ChannelPool。 ChannelPools:ChannelPools用于缓存ChannelPool。在RestClient中,当一个Server host长时间没有被访问时,其所对应的ChannelPool将会被视作缓存过期,从而被回收资源。 4.2.6.2EventLoop&EventLoopGroup EventLoop:EventLoop在Netty中被用来运行任务来处理在Channel的生命周期内发生的事件。在RestClient中,一个EventLoop对应了一个线程。 EventLoopGroup:EventLoopGroup为一组EventLoop,其保证将多个任务尽可能地均匀地分配在多个EventLoop上。 4.2.6.3Epoll Epoll是Linux内核的可扩展I/O事件通知机制,包含下面这三个系统调用。 int epoll_create(int size); 在内核中创建epoll实例并返回一个epoll文件描述符(对应上图中 EpollEventLoop中的epollFD)。在最初的实现中,调用者通过 size 参数告知内核需要监听的文件描述符数量。如果监听的文件描述符数量超过 size, 则内核会自动扩容。而现在 size 已经没有这种语义了,但是调用者调用时 size 依然必须大于 0,以保证后向兼容性。 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 向 epfd 对应的内核epoll 实例添加、修改或删除对 fd 上事件 event 的监听。op 可以为 EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL 分别对应的是添加新的事件,修改文件描述符上监听的事件类型,从实例上删除一个事件。 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 当 timeout 为 0 时,epoll_wait 永远会立即返回。而 timeout 为 -1 时,epoll_wait 会一直阻塞直到任一已注册的事件变为就绪。当 timeout 为一正整数时,epoll 会阻塞直到计时 timeout 毫秒终了或已注册的事件变为就绪。因为内核调度延迟,阻塞的时间可能会略微超过 timeout 毫秒。 Epoll运作流程: 1、进程先通过调用epoll_create来创建一个epoll文件描述符(对应上图中 EpollEventLoop中的epollFD)。epoll通过mmap开辟一块共享空间,该共享空间中包含一个红黑树和一个链表(对应上图epollFD中对应的Shared space)。 2、进程调用epoll的epoll_ctl add,把新来的链接的文件描述符放入红黑树中。 3、当红黑树中的fd有数据到了,就把它放入一个链表中并维护该数据可写还是可读。 上层用户空间(通过epoll_wait)从链表中取出所有fd,然后对其进行读写数据。 4.2.6.4NettyTransceiver初始化 当RestClient刚完成初始化时,NettyTransceiver也刚完成初始化,其初始化主要包含下面两部分: · 初始化 ChannelPools,刚初始化的ChannelPools为空,其内部不含有任何ChannelPool。 · 初始化EpoolEventLoopGroup, EpoolEventLoopGroup包含多个EpoolEventLoop。每个EpoolEventLoop都包含下面这三个部分: executor:真正执行任务的线程。 taskQueue:任务队列,用户要执行的任务将被加入 到该队列中,然后再被executor执行。 epollFD:epoll的文件描述符,在EpoolEventLoop创建时,调用epoll_create来创建一个epoll的共享空间,其对应的文件描述符就是epollFD。 4.2.6.5NettyTransceiver发送请求 当第一次发送请求时:NettyTransceiver将会为该Server host创建一个ChannelPool(如上图中的ChannelPool1),并缓存到channelPools中(默认10分钟内该Server host没有请求则视为缓存过期,其对应的ChannelPool将被从channelPools中删除)。ChannelPool在初始化时,主要包含下面两部分: · 初始化channelDeque,用于缓存channel,获取channel就是从channelDeque中拿出一个channel。 · 在EventLoopGroup中选定一个EventLoop作为executor,该executor用来执行获取连接等操作。之所以ChannelPool需要一个固定的executor来执行获取连接等操作,是为了避免出现多个线程同时获取连接的情况,从而不需要对获取连接的操作进行加锁。 ChannelPool初始化完成后,则将由executor从ChannelPool中获取channel,初次获取时,由于ChannelPool中还没有channel,则将初始化第一个channel,channel的初始化步骤主要包含下面几步: · 创建连接,将连接封装为channel。 · 将channel对应的连接通过epoll_ctl add方法加入到EpollEventLoopGroup中的一个EpollEventLoop的epollFD对应的共享空间的红黑树中。 · 将channel放到对应ChannelPool的channelDeque中。 初始化channel完成后,executor则将初始化好的channel返回,由绑定该channel的EpollEventLoop(即初始化channel第二步中所选定的EpollEventLoop)继续执行发送请求数据的任务。 4.2.6.6NettyTransceiver接收响应 4.2.7.1 线程模型的进一步优化 上面的线程模型为我们当前版本的线程模型,也是Netty自带连接池的线程模型。但是这种线程模型的性能一定是最高的吗? 这个问题的答案应该是否定的,因为尽管 ChannelPool 中指定一个 EventLoop 作为 executor 来执行获取 Channel 的操作可以使得 获取Channel 的过程无多线程争抢,但是却引入了下面这两个问题: · 获取Channel到Channel.write()之间大概率会进行一次EventLoop 切换 (有可能会将 获取Channel 与 Channel.write()分配到同一个EventLoop ,如果分配到同一个EventLoop,则不需要进行EventLoop 切换 ,所以这里说大概率会切换),这次切换是有一定的性能成本的。 · EventLoopGroup中的 EventLoop任务分配不均匀。因为channelPool中获取连接的那个 EventLoop在获取连接的同时还要处理数据的收发,比其他EventLoop多做一些工作,该EventLoop也成为了性能瓶颈点。在我们实际测试当中,也的确发现有一个EventLoop的线程CPU利用率较其它EventLoop更高一些。 那么更优越的线程模型是怎样的呢?通过上面的分析,我们觉得它应该要满足下面两点: · 获取Channel 到 Channel.write()之间无线程切换。 · 每个EventLoop的任务分配均匀。 基于我们的需求,我们可以得出最佳的结构模型与线程模型应该为下面这种: 优化后的结构模型: 如上图所示:一个 ChannelPool 由多个 ChildChannelPool 构成(个数 = IO线程个数),一个ChildChannelPool与一个 EventLoopGroup绑定,该EventLoopGroup仅含有一个 EventLoop (即一个ChildChannelPool对应一个EventLoop)。 优化后的线程模型: 如上图所示:先在业务线程中执行一些操作并获取 ChannelPool,及选取一个ChildChannelPool(选取的实现类似于EventLoopGroup.next()实现,其保证了ChildChannelPool的均匀分配),然后通过ChildChannelPool来获取Channel(该过程在ChildChannelPool对应的EventLoop中执行),然后调用Channel.write() (该过程也在ChildChannelPool 对应的 EventLoop 中执行) 。 上述过程巧妙的达成了我们一开始所需要的高性能线程模型的两点: · 获取Channel 到 Channel.write()之间无线程切换 —— 由于ChildChannelPool 中的EventLoopGroup仅有一个EventLoop ,其创建的Channel 也只能绑定该EventLoop ,因此获取Channel与Channel.write()都只能在该EventLoop 种执行,从而没有了线程切换。 · 每 个 EventLoop任务分配均匀 —— 由于ChildChannelPool 是被均匀地从 ChannelPool 中获取的(该过程与EventLoopGroup.next()的过程类似),而一个ChildChannelPool 刚好对应了一个EventLoop ,从而使得请求任务被均匀分配。 实践中我们也通过一个Demo进行了测试:发现采用上面这种线程模型与结构模型,使得 RestClient的性能在当前版本的基础上又提升了20%左右。预计下个版本中RestClient将会提供上面这种线程模型与结构模型。 其它性能优化的一些设计 5、Netty RestClient基于Netty编写,Netty自带的一些高性能特性自然是RestClient高性能的基石,Netty常见特性均在RestClient中有所运用: · Epoll · Channel&ChannelPool · EventLoop&EventLoopGroup · ByteBuf&PooledByteBufAllocator · Future&Promise · FastThreadLocal&InternalThreadLocalMap · ... 其中:Epoll、 Channel & ChannelPool、EventLoop & EventLoopGroup 我们在该篇文档的结构设计部分已经有过讲解,这里不再对其做过多解释,下面我们主要来看看其它几个部分: 5.1ByteBuf&PooledByteBufAllocator Netty使用了即易于使用又具备良好性能的ByteBuf来替代ByteBuffer。这里不对ByteBuf进行详细的介绍,主要简单介绍RestClient中如何利用ByteBuf来提高性能以获得更好地用户体验: · 发送请求时,使用PooledByteBufAllocator来分配ByteBuf,其池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。 · 接收响应时,使用CompositeByteBuf,它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示,减少了当响应分批次到来时聚合响应产生的不必要地数据拷贝。 5.2Future&Promise Future & Promise 为Netty异步的基石,这里不对Future & Promise 进行详细的介绍,主要介绍RestClient中对于Future & Promise 一些相关的技术上取舍。 RestClient利用Future & Promise来实现数据包收发时的异步,并在面向用户时将Future & Promise转化成CompletionStage。由此实现了从数据包收发 到用户编解码的整个请求链路的异步化。 5.3 WhyCompletionStage,Not Future & Promise? CompletionStage是Java8新增的一个接口,用于异步执行中的阶段处理,其大量用在Lambda表达式计算过程中,目前只有CompletableFuture一个实现类。 比起Netty的Future & Promise,Java开发者更加熟悉CompletionStage,且CompletionStage的接口功能也更加强大,用户可以借其更加灵活地实现业务逻辑。 5.4WhyCompletionStage,Not CompletableFuture? 之所以使用CompletionStage 而不使用CompletableFuture。是因为 CompletionStage 是接口,而CompletableFuture为 CompletionStage 的实现,使用 CompletionStage 更符合面向接口编程的原则。同时用户也可以使用CompletionStage.toCompletableFuture()来将CompletionStage转化为CompletableFuture。 5.5 How To CombineFuture&Promise With CompletionStage? 在用户调用请求发送时,我们构建了一个CompletionStage,并在执行Netty处理请求与响应逻辑返回的Future中增加Listener,在该Listener中结束CompletionStage。通过这样实现了将 Future & Promise 与 CompletionStage 结合,从而实现整个请求链路的异步化。 对这块感兴趣的用户可以查看io.esastack.httpclient.core.netty.HttpTransceiverImpl中的handle()方法,该方法中完成了 Future到CompletionStage的转化。 5.6FastThreadLocal&InternalThreadLocalMap FastThreadLocal通过将ThreadLocal中使用哈希结构的ThreadLocalMap改为了直接使用数组结构的InternalThreadLocalMap。ThreadLocal与FastThreadLocal结构图大致如下: ThreadLocal结构图 FastThreadLocal结构图 如上图所示,比起ThreadLocalMap,InternalThreadLocalMap直接根据 index 来获取值、设置值的做法更加简单,且直接使用数组的复杂度更低(虽然 ThreadLocalMap也是数组结构,但是其在数组的存取操作外还封装了大量hash计算以及防止hash碰撞的相关操作)。因此FastThreadLocal获得了更高的性能。 RestClient中均使用FastThreadLocal代替ThreadLocal以获取更高的性能。 5.7 Encode & Decode 与大多数Http客户端框架不同,RestClient不仅支持将Java对象Encode成byte[],还支持将Java对象Encode成其他底层Netty支持的对象,如:File、MultipartBody等,未来还将会支持ChunkInput用来支持将请求体比较大的请求分块发送。 之所以这么设计,是因为如果我们仅仅支持将Java对象Encode成byte[],那么当Encode后的byte[]数据过大时,将会出现OutOfMemoryException。用户在发送大文件或者本身请求体较大的请求时,都会出现这个问题。 为了解决这个问题,RestClient通过让用户可以将Java对象Encode成File或者ChunkInput来解决这一类问题。当用户将Java对象Encode成File时,RestClient将会借助底层的Netty使用NIO以零拷贝的方式发送文件,避免了OOM的同时又减少了数据的多次拷贝。 同理当用户将Java对象Encode成ChunkInput时,RestClient将会分块发送数据,避免数据一次性全部加载进内存,从而避免OOM的情况。(PS:ChunkInput当前版本暂不支持,但已留出扩展点,将在下一版本支持) Decode时也做了同样的优化,由于原理相同这里就不再展开讲解了。 6、结语 尽管RestClient主要只涉及发起请求这个简单的功能,但是“麻雀虽小,五脏俱全”,它考虑到了性能优化的方方面面,同时在 接口设计、代码整洁、功能完善 几个方面上也尽量做到了毫不妥协。 它还是一个年轻的项目,欢迎各路技术爱好者们加入,一同探讨学习与进步。 作者简介 Without OPPO云计算中心云原生组后端工程师 深度参与ESA Restlight、ESA RestClient、ServiceKeeper等多个高性能开源框架项目 推荐阅读 |开源高性能Web服务框架ESA Restlight |YARN的介绍及实践探索 本文分享自微信公众号 - OPPO数智技术(OPPO_tech)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

ElasticSearch的REST APIs 之 索引的别名(alias)管理

基于 ES 7.7, 官方文档 注意: 7.15版本中调整了分类, 把7.7版本中的Add和Update合并为"Create or Update", 更符合es api的定义 主要内容: Add index alias: 创建或更新别名 Delete index alias: 删除索引别名 Get index alias: 获取索引别名 Index alias exists: 检查索引别名是否存在 Update index alias: 添加/删除索引别名 1. Add index alias: 创建或更新索引别名 索引别名, 是一个或多个索引的辅助名称。 大多数 ES api 都接受使用索引别名来代替索引名称。官方文档链接 请求方式 # 支持PUT, POST方式 PUT|POST /<index>/_alias/<alias> # 用 _aliases 也可以的, 但是我们通常用 _alias, 字母能少写一个是一个 PUT|POST /<index>/_aliases/<alias> 当然也可以使用 `_all`来给当前集群的所有索引添加别名 => 这个有点不正常了 `PUT|POST` /_all/_alias/<alias> 查询参数 master_timeout (可选,时间单位)连接到主节点的等待超时时间。如果在超时时间已过之前没有响应, 则返回错误。 默认是值 30s。 timeout (可选,时间单位) 等待超时时间。如果在超时时间已过之前没有响应, 则返回错误。 默认是值 30s。 Request body filter (Required, query object) . 筛选查询(Filter query) 用来限制索引别名, 使用该别名时只返回符合过滤条件的文档。(这个应该是有body的时候才是必填的, 没有body就不用填写了) 这个有点像mysql中的view, 同一个table可以建立很多个view, 可以组合不同table的数据, 也可以给不同权限的账号建立不同的view来限制其可以访问的数据 routing (Optional, string) 用于将操作路由到特定分片的自定义路由。 几个栗子 1. 添加基于时间(time-based)的别名 比如给索引logs_20302801创建一个别名2030: PUT /logs_20302801/_alias/2030 2. 添加基于用户(user-based)的别名 创建索引users, 它有一个字段user_id, 然后给个别名user_12, 限制user_id=12的文档: PUT /users { "mappings": { "properties": { "user_id":{"type": "integer"} } } } # 插入几个文档 POST /users/_doc/10 { "user_id": 10 } POST /users/_doc/11 { "user_id": 11 } POST /users/_doc/12 { "user_id": 12 } # 查询, 能看到所有的文档 GET /users/_search # 给个别名`user_12`, 限制user_id=12的文档: PUT /users/_alias/user_12 { "routing": "12", "filter":{ "term":{ "user_id": 12 } } } # 查询别名, 只能看到 user_id=12的文档 GET /user_12/_search 官方文档举的这个栗子, 朕觉得不是太合适。maybe用"订单列表" 索引, 然后根据订单状态去建别名更适合 3. 在创建索引时添加别名 建立索引 logs_20302801, 同时添加别名 2030 PUT /logs_20302801 { "mappings" : { "properties" : { "year" : {"type" : "integer"} } }, "aliases" : { "current_day" : {}, "2030" : { "filter" : { "term" : {"year" : 2030 } } } } } 2. Delete index alias: 删除索引别名 DELETE /<index>/_alias/<alias> # 用 _aliases 也可以的, 但是我们通常用 _alias, 字母能少写一个是一个 DELETE /<index>/_aliases/<alias> 路径参数 <index> 支持多个索引以英文逗号连接, 支持通配符。 要匹配所有索引, 可以使用 _all 或者 * # 删除多个索引的别名 DELETE /twitter,users,orders/_alias/<alias> # 删除所有索引的别名 ---这个比较危险, 请勿随意测试 DELETE /_all/_alias/<alias> DELETE /*/_alias/<alias> <alias> 支持多个索引以英文逗号连接, 支持通配符。 要匹配所有索引别名, 可以使用 _all 或者 * # 删除指定索引的多个别名 DELETE /<index>/_alias/alias1,alias2,alias3 # 按通配符删除指定索引的别名 DELETE /<index>/_alias/ab* # 删除指定索引的所有别名 DELETE /<index>/_alias/* 查询参数 master_timeout (可选,时间单位)连接到主节点的等待超时时间。如果在超时时间已过之前没有响应, 则返回错误。 默认是值 30s。 timeout (可选,时间单位) 等待超时时间。如果在超时时间已过之前没有响应, 则返回错误。 默认是值 30s。 3. Get index alias: 获取索引别名 获取一个或多个索引别名。 官方文档 GET /_alias GET /_alias/<alias> GET /<index>/_alias/<alias> 路径参数 <alias> 支持多个索引以英文逗号连接, 支持通配符。 要匹配所有索引别名, 可以使用 _all 或者 * <index> 支持多个索引以英文逗号连接, 支持通配符。 查询参数 allow_no_indices 默认true。如果设置为true, 则当全部使用通配符*、_all只检索不存在(missing)或者已关闭(closed)的索引时,不会抛出错误。 这个是和参数 expand_wildcards 一起使用时的描述。如果只用通配符去检索expand_wildcards=all的别名, 则无论如何不会抛出错误。但是这里的missing条件怎么理解呢 ?? # 只是通配符检索别名: 不存在user2开头的别名, 不会报错(返回状态200, 空json) GET /_alias/user2*?allow_no_indices=false # 检索不存在的user2, 以及user2开头的别名时, 一定会报错(404 + 错误信息json) GET /_alias/user2?allow_no_indices=true GET /_alias/user2,user2*?allow_no_indices=true # 会报错 - 这个才是符合描述的(条件 expand_wildcards=closed) GET /_alias/user2*?allow_no_indices=false&expand_wildcards=closed expand_wildcards 通配符查询时的范围限制. 支持多个条件的逗号连接. all: 默认, 匹配open和closed的索引, 包括隐藏的. open: 表示只查询开放中的索引 closed: 只匹配closed的 hidden: 隐藏的(hidden)索引, 必须和open/closed联合使用. none: 不接受通配符. 默认 all ignore_unavailable 默认false. 如果设置为true, missing或closed的索引不会包括在返回数据中. (官方文档的这种表达, 至今还未理解是什么意思...) local 是否仅从本地节点获取信息 默认false, 表示从master节点获取信息. 如果设置为true, 则表示只从本地节点获取. 栗子 获取当前集群下的所有别名。返回所有索引, 无别名的显示空json GET /_alias GET /_all/_alias GET /*/_alias 返回的json中包含当前集群的所有index, 但是每次返回的顺序可能不同. 获取所有索引中有指定别名的,多个别名以英文逗号连接。 只要有一个别名不存在时抛出错误, 返回404状态, 以及能匹配到的部分索引 比如: 存在别名 user_12 和 2030, 不存在别名user_13 GET /_alias/user_13 GET /_alias/user_12,user_13,2030 通配符匹配指定的索引别名, 没有数据时返回空json # 查询所有索引的所有以ab开头的别名 GET /_alias/ab* # 查询索引user的所有别名 GET /users/_alias/* # 查询索引user的所有以ab开头的别名 GET /users/_alias/ab* 通配符*的位置任意放, 也可以有多个* 4. Index alias exists: 检查索引别名是否存在 检查一个或多个索引别名是否存在. 官方文档 HEAD /_alias/<alias> HEAD /<index>/_alias/<alias> 路径参数 <alias> 支持多个索引以英文逗号连接, 支持通配符。 <index> 支持多个索引以英文逗号连接, 支持通配符。 查询参数 同索引别名的获取, 这里省略.... 5. Update index alias: 添加/删除索引别名 添加或删除索引别名。 POST /_aliases { "actions": [ { "add": { "index": "twitter", "alias": "twitter_alias1" } } ] } 查询参数 master_timeout (可选,时间单位)连接到主节点的等待超时时间。如果在超时时间已过之前没有响应, 则返回错误。 默认是值 30s。 timeout (可选,时间单位) 等待超时时间。如果在超时时间已过之前没有响应, 则返回错误。 默认是值 30s。 Request body ES 7.15 对本节内容进行了修正, 结构和描述都更清晰(但是属性列表按字母排序了, 而不是优先级排序), 详见官方文档 actions (必填, json数组对象) 要执行的一系列操作, 可用的action(json的key)包括: add 给索引添加一个别名 remove 从指定的索引移除一个别名 remove_index 删除一个索引或索引别名, 类似删除索引api 这些action的属性可以为空. 可用的属性包括: index: 带通配符的表达式. 当参数indices没有指定时必填. indices: 索引名称数组. 当参数index没有指定时必填. alias: 逗号连接的多个别名, 或带通配符的表达式. 当参数aliases没有指定时必填. aliases: 索引别名的数组. 当参数alias没有指定时必填. filter: 使用Filter query过滤别名. is_hidden: 如果设置为true, 默认将从通配符表达式中排除别名,除非在请求中使用expand_wildcards参数重写别名,类似于hidden indices。对于共享一个别名的素有索引来说, 这个属性必须设置成相同的值。默认是false。 is_write_index: 如果为true,则将此索引指定为该别名下的写索引。 默认值为false。 一个别名同时只能存在一个写索引。参考write index routing: 用于将操作路由到特定分片的自定义路由值。 参考 Routing index_routing: 用于别名索引操作的自定义路由值。 search_routing: 用于别名搜索操作的自定义路由值。 栗子 1. 添加别名(Add an alias) # 添加别名 POST /_aliases { "actions":[ { "remove":{ "index": "my_index", "alias": "my_index_alias1" } } ] } 2.移除一个别名(Remove an alias) # 移除别名 POST /_aliases { "actions":[ { "remove":{ "index": "my_index", "alias": "my_index_alias1" } } ] } 3. 别名重命名(Rename an alias) Rename = Remove + Add 重命名实际上是两个操作: 先删除, 后添加, 可以一起操作. 而且这是一个原子操作, 不会出现短时间内别名无法使用的情况 # 别名重命名: 删除 + 添加 POST /_aliases { "actions":[ { "remove":{ "index": "my_index", "alias": "my_index_alias1" } }, { "add":{ "index": "my_index", "alias": "my_index_alias_1" } } ] } 4. 同时给多个索引添加别名 同时给多个索引添加不同的别名 # 同时给多个索引添加不同的别名(这个写法也支持添加相同的别名) POST /_aliases { "actions":[ { "add": { "index": "my_index", "alias": "my_index_alias1" } }, { "add":{ "index": "my_index2", "alias": "my_index2_alias1" } } ] } 同时给多个索引添加相同的别名 同时给多个索引添加相同的别名 POST /_aliases { "actions":[ { "add":{ "index": "my_index", "alias": "alias1" } }, { "add":{ "index": "my_index2", "alias": "alias1" } } ] } 同时给多个索引添加相同的别名时, 可以直接设置一个indices数组以取代多个add操作: # 同时给多个索引添加相同的别名, 可以使用indices数组以取代多个add操作 POST /_aliases { "actions":[ { "add":{ "indices": ["test1", "test2"], "alias": "alias1" } } ] } 也可以对index使用通配符来同时给一批索引添加同一个别名: # 添加多个索引 PUT /test1 PUT /test2 PUT /test3 # 查看索引 GET /test* # 批量添加别名 POST /_aliases { "actions":[ { "add":{ "index": "test*", "alias": "all_test_indices" } } ] } # 查看索引, 我们会看到这三个以test开头的索引都有一个相同的别名 GET /test* 我们还可以交换(swap)一个索引和一个索引别名的名字, 而且是一个原子操作, 这不会出现短时间内别名无法使用的情况. # 我们先建了一个索引`log`, 但是这个是错误的 PUT /log # 现在建立了正确的索引`log_001`, 但是我现在想把那个索引名称`log`给这个当别名 PUT /log_001 # 我们可以使用如下的原子操作, 保证服务不会中断 POST /_aliases { "actions":[ { "add": { "index": "log_001", "alias": "log" } }, { "remove_index":{ "index": "log" } } ] } # 查看索引/别名, 我们我们会发现别名log也指向了log_001 GET /log_001,log 5. 过滤别名(Filter aliases) 带过滤器的别名提供了一种简单的创建一个索引的不同“视图”的方法。 该过滤器可以使用Query DSL定义,并应用于使用此别名的所有搜索、计数、按查询删除等操作。 我们先创建一个索引, 然后使用filter条件建立一个索引别名 # 创建一个索引, 包含字段 user PUT /test1 { "mappings": { "properties": { "user":{ "type": "keyword" } } } } # 然后我们对字段user建"视图": 使用条件 user="kimchy" 的全匹配, 添加过滤别名 POST /_aliases { "actions":[ { "add":{ "index": "test1", "alias": "test1_kimchy", "filter":{ "term":{ "user": "kimchy" } } } } ] } 好了, 现在如果我们查询索引别名test1_kimchy, 则只显示test1中 user="kimchy" 的文档 6. 路由(Routing) 可以将路由值(routing values)与别名关联起来, 还可以与过滤别名(filter aliases)一起使用以减少不必要的分片操作. 我们给索引test建一个别名alias1, 定义这个别名的操作都自动指向路由值为"1"的分片. POST /_aliases { "actions":[ { "add":{ "index": "test", "alias": "alias1", "routing": "1" } } ] } 也可以按操作区分搜索(searching)和索引(indexing)分别指向不同的路由: POST /_aliases { "actions":[ { "add":{ "index": "test", "alias": "alias1", "search_routing": "1,2", "index_routing": "1" } } ] } 上面的例子中,搜索路由包含多个用逗号分隔的值,索引路由只包含一个值。 If a search operation that uses routing alias also has a routing parameter, an intersection of both search alias routing and routing specified in the parameter is used. For example the following command will use "2" as a routing value: 如果使用路由别名的搜索操作也有路由参数,则使用搜索别名路由和参数中指定的路由的交集。 例如,下面的命令将使用"2"作为路由值: GET /alias2/_search?q=user:kimchy&routing=2,3 alias2的search_routing是"1,2", 而上面的搜索命令中指定"2,3", 他们的交集是 "2" 7. 写索引(Write index) 可以关联设置别名所指向的索引为写索引。 指定后,指向多个索引的别名的所有索引(index, 动词)和更新(update)请求将尝试解析到那个写索引。 每次只能为每个别名分配一个索引作为写索引。 如果没有指定写索引,并且别名引用了多个索引,则不允许写。 可以使用aliases API和创建索引API将与别名关联的索引指定为写索引。 Setting an index to be the write index with an alias also affects how the alias is manipulated during Rollover 通过别名来设置一个索引为写索引会影响在Rollover期间如何操作别名, 参考 Rollover With Write Index POST /_aliases { "actions":[ { "add":{ "index": "test", "alias": "alias1", "is_write_index": true } }, { "add":{ "index": "test2", "alias": "alias1" } } ] } 这个例子中, 我们把别名alias1同时执行索引test和test2, 但是只指定索引test为写索引. PUT /alias1/_doc/1 { "foo": "bar" } 然后通过别名alias1的写和索引操作将指向索引test, 上面的代码会把id=1的文档写入索引test中: GET /test/_doc/1 To swap which index is the write index for an alias, the Aliases API can be leveraged to do an atomic swap. The swap is not dependent on the ordering of the actions. 要在一个别名所指向的多个索引之间交换写索引的设置, 可以利用Aliases API来执行原子交换。 交换不依赖于操作的顺序。 POST /_aliases { "actions":[ { "add": { "index": "test", "alias": "alias1", "is_write_index": false } }, { "add": { "index": "test2", "alias": "alias1", "is_write_index": true } } ] } last update at 2021/10/25 23:17

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册