# 一、挑战/注意事项

【1】开启从上海到新加坡的流量之前,务必检查应用此前是否开启过新加坡流量到上海的“虫洞”。上海路由到新加坡,新加坡全量再回上海,会导致死循环,体现在客户端调用上,就是全部调用超时。
【2】Service Mesh开启时,无法正常使用UCS调度。关闭Service Mesh后,必须重新发布应用才能生效。
【3】目前SHA员工可以同时查看国内和国外的数据,而国内外的数据时拆分存储的,就需要国内系统请求国外的数据库进行数据整合。涉及分页、多订单请求拆分国内国际、多IDC数据整合等问题。

# 二、SOA

# SOA运行机制

AWS

# SOA路由机制

【1】路由算法: 就近访问、加权轮询
【2】适用范围: 请求维度 + 环境维度, 请求维度分为:操作、请求标识。环境维度分为:AppID、子环境。
【3】路由目标: 应用分组、应用 + IDC分组、CMS Group分组、实例分组(逻辑实例)

路由的作用: 服务拆分、服务Mock、跨区域请求等。

# 熔断 Circuit Breaker

熔断条件:
【1】在最近连续10s内,请求数大于触发熔断最小请求数20s(默认值);
【2】在最近连续10s内,请求失败率 >= 失败率阈值50%(失败率 = (错误请求数 + 超时请求数)/ 总请求数);

如何恢复:
【1】保持一段时间(默认值:5s)的熔断状态;
【2】放行一个请求进行验证:a)验证通过则熔断恢复;b)验证不通过则回到步骤1

生效粒度:
【1】服务端:操作(具体的方法)
【2】客户端:服务 + 操作 + 服务端IP

AWS

# 隔离 Isolation

目的:避免部分请求占据过多系统资源,通过信号量 + 线程池 控制并发的数量,单机默认为300;

# 限流

当最近10s内一类请求的数量超过特定阈值时,服务会直接返回429 Too Many Requests,如果配置了多种限流,就需要所有条件都满足才可以通过;
维度:1)服务;2)操作;3)调用方APPID;4)调用方APPID+操作;5)调用方IP
粒度:1)单机默认100;2)集群(伪);

# 黑白名单

行为:当请求不满足黑白名单要求时,服务段返回403 Forbidden,如果配置了多种名单,就需要所有的条件都满足才可以通过;
类型:1)操作;2)AppID;3)IP(不推荐);4)BU(仅白名单);
维度:1)服务;2)操作;

# 超时 TimeOut

超时类型:
【1】Connection Request Timeout:从连接池中获取连接的超时,默认值200ms;
【2】Connection Timeout:与服务端建立TCP连接的超时,默认值1200ms;
【3】Socket Timeout:每次在Socket上调用write/read的超时时间,默认值4000ms;

配置方式:SOA Portal方便管理;
配置优先级:客户端 > 服务端, 操作粒度 > 服务粒度;

# 三、Service Mesh

服务网络(Service Mesh)是处理服务间通信的基础设施层。如果SOASDK要升级熔断、限流等,就需要系统代码层进行Maven升级,对于业务而言工作量是非常庞大的,如果SDK出现了BUG还需要系统进行回退,后果是非常严重的。Service Mesh就是将这部分内容与应用进行解耦,剥离到中间层进行独立的迭代。负责构成现代云原生应用程序的复杂拓扑来可靠地交付请求。Service Mesh通常以轻量级网络代理阵列的形式实现,这些代理与应用程序代码部署在一起,对应用程序来说无需感知代理的存在

AWS

特点:1)应用程序间通信的中间层;2)轻量级网络代理;3)应用程序无感知;4)解耦应用程序的重试/超时、监控、追踪和服务发现;

Service Mesh对于研发的好处:
【1】功能增强:Service Mesh中的功能比现有的SOA更多,未来功能迭代也会更快;
【2】语言互通:无论是Java调用小众语言服务还是小众语言调用Java服务,使用的体验一致,互调会更方便;
【3】升级无感:Service Mesh Sidecar的升级不需要业务介入,发布系统就可以完成;

Service Mesh对于Java使用者:无缝过渡:不改变开发体验,继续沿用现有SOA SDKSOA SDK底层会做简化,减少发布频率;

对多语言使用者的好处:
【1】体验升级:可以得到几乎和Java一样SOA开发体验;
【2】性能体验:不需要WebAPI,也不会局限HTTP。对性能有更高要求的还可以使用gRPCgRPC Streaming来提升性能。
【3】监控提升:不再强依赖CAT,使用开源产品上报数据也可以和CAT做整合。

AWS

接入Service Mesh后一些行为变化:

功能点 接入前 接入后
谁负责注册服务实例 SOA SDK SOA Operator
谁负责执行路由 SOA SDK Sidecar
谁负责执行负载均衡 SOA SDK Sidecar
谁负责执行治理功能(黑白名单、限流、熔断) SOA SDK Sidecar

# 四、服务网关的路由机制

# SLB

SLB(Software Load Balancer)软负载均衡,主要用于为使用HTTP协议的请求提供路由和负载均衡功能。

AWS

SLB访问入口常见错误:
【1】499 Client Closed Request:客户端超时;
【2】502 Bad Gateway:应用无可用服务器或服务器连接失败;
【3】504 Gateway Timeout:应用服务器响应过慢;
【4】NET::ERR_CERT_AUTORITY_INVALID:未安装根证书;
【5】相同URL随机404:未DR部署;

# Gateway

Gateway作为业务服务网关系统,主要职责是将内网服务暴露至与之隔离的网络环境(外网、专线、办公网络等)

协议 名称 使用场景/目标用户
HTTP H5 Gateway 处理H5/Online/App前端用户从外网发来的请求
HTTP PCI Gateway 用户与H5 Gateway相同,处理访问PCI服务请求
HTTP Affiliation Gateway 处理来自合作方的请求,支持公网和专线两种调用方式
HTTP Offline Gateway 处理内网Offline站点前端用户发来的请求
HTTP Corp Gateway 处理来自银行等专线发来的请求
TCP(SOTP) TCP Gateway 处理APP前端Native请求

操作白名单:适用于在Gateway上开发了访问入口的SOA服务,防止接口意外暴露至外网,进而产生敏感数据泄露等信息安全风险。新接口是不会自动加入白名单。

# Gateway 非路由功能

【1】CORS: 跨域资源共享(Cross-Origin Resource Sharing, CORS)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止JavaScript代码获取跨域请求的响应。
【2】反爬: 支持配置基于请求数计算的反爬策略。维度:客户端IP/Clinet ID/IP + ClientID。支持反爬的GatewayH5 Gateway/TCP Gateway/PCI Gateway。生效粒度:应用粒度和SOA服务粒度。反爬操作:黑名单(拒绝请求并返回错误信息)灰名单(要求填写验证码校验)标记并转发。
【3】访问控制: 1)IP白名单,适用的网关:Affiliation GatewayCorp Gateway; 2)以服务粒度配置;
【4】基于Token的验证机制:1)支持的网关Affiliation Gateway; 2)以服务粒度配置;3)通过AppKey+AppSecret动态获取当前Token

# 五、SLB 与 Gateway 的区别

SLB Gateway
核心功能是路由和负载均衡 核心功能是路由
转发目标是一组服务器 转发目标是一个URL
基本不涉及任何业务逻辑 内含一些业务逻辑:跨域请求处理/用户认证与授权/请求加解密/请求协议转换
只做同协议请求转发

# 六、WebApi

WebApiSOA服务提供了可通过域名访问的访问入口:
【1】路由功能与SDK基本对齐;
【2】按需申请并开放访问;

主要应用场景:Gateway访问;

副作用:1)配置同步链路较长;2)运维成本高;

AWS

# 七、跨区域服务访问的运行机制

# 虫洞

“虫洞”是针对跨区域、跨租户的互访需求所设置的专用网络链路。支持HTTP/HTTPS/gRPC协议。

AWS

# 基于 UDL 的流量调度

UDL(User Data Location)是用户数据的归属地,取自用户注册海外账号时站点的区域(Locale)设置后两位。便于维护和数据合规。下发规则:仅对海外用户下发,用户登录时或登录后启动应用时下发,下发后保存在客户端本地。

特点:用户粘滞、百分之一灰度、支持白名单。

AWS

输入:VID/CID/UID+UDL;输入方式:直接映射/哈希取模分段映射等;输出:负责处理该请求的区域(BJ、SIN等);

标识类型 生成位置 下发事件点 适合场景
VID 客户端 无需下发 登录前/按客户端分流
CID 服务端 前端主动请求服务端获取 同VID,但CID覆盖率低于VID
UDL 服务端 用户登录完成后 登录后/按用户分流

# 八、跨用户请求

对于某些单个请求或响应中含有多个用户信息的服务,SDK提供了一套基于统一的UCS拆分和聚合的解决方案供开发者使用。

# 请求拆分

对于跨用户服务的请求,我们提供了两个处理方案:
【1】根据用户信息拆分请求: 场景:请求内含有对应多个用户的对象列表。例如批量查询,批量匹配订单进行批量操作。

Map<SwitchTag, R> split(R originalRequest,  //  原始的请求RequestType。
                        String splitItemCollectionFieldName,  // 请求内含有多个用户信息的对象集合,由于契约限制必须为List类型。
                        Function<T, K> splitKeyGetter,  // 获取上述多用户对象集合内用来分割请求的key,支持的类型参照上文MappingFieldType的类型。
                        MappingFieldType keyType) throws RequestSplitException;   // 分割请求的key对应的类型
1
2
3
4

示例用法:以特殊事件强绑接口为例,EditForceMatchedOrderRequest中,forceMatchedOrderList内可能会包含多个不同用户的订单,且对象内含有订单号的信息,可以用来匹配用户的uid。代码如下:


MultiUserRequestSplitter splitter = MultiUserRequestSplitterImpl.getInstance();
EditForceMatchedOrderRequest request = new EditForceMatchedOrderRequest();try {
    Map<SwitchTag, EditForceMatchedOrderRequest> splitRequests =
    splitter.split(request,
                   "forceMatchedOrderList",
                   ForceMatchedOrder::getOrderId,
                   MappingFieldType.FLIGHT_ORDER_ID);
   
} catch (RequestSplitException e) {
    // exception process
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

【2】广播请求至所有Region 场景:请求中不含有用户信息,但是返回结果会存在多个用户的数据。例如最终行程匹单,利用规则ID查询所有匹配特殊事件规则的订单。

Map<SwitchTag, R> broadcast(R originalRequest) throws RequestSplitException;
1

用户只需要提供原始的请求,该方法就会将该请求复制多份到每个region

以查询强绑订单为例,QueryForceMatchedOrderRequest中,可以只传入configId,匹配所有符合该id的订单。代码如下:

MultiUserRequestSplitter splitter = MultiUserRequestSplitterImpl.getInstance();
QueryForceMatchedOrderRequest request = new QueryForceMatchedOrderRequest();try {
    Map<SwitchTag, QueryForceMatchedOrderRequest> splitRequests = splitter.split(request);
} catch (RequestSplitException e) {
    // exception process
}
1
2
3
4
5
6
7
8

# 请求执行

SDK中提供了标准的api可以让开发者方便的执行被拆分出来的请求。API列表如下:

List<RequestExecutionContext<R,P>> execRequests(Map<SwitchTag, R> requestMap,
                                                Class<P> responseClz,
                                                C serviceClient,
                                                String operationName) throws RequestExecutionException;RequestExecutionContext<R,P> execRequest(SwitchTag switchTag,
                                         R request,
                                         Class<P> responseClz,
                                         C serviceClient,
                                         String operationName) throws RequestExecutionException;
1
2
3
4
5
6
7
8
9
10

大部分情况下,开发者只需调用execRequests方法,传入上述拆分功能返回的请求列表,以及调用客户端相关信息即可。当且仅当开发者对调用顺序有严格要求,或需要对每次请求单独做自定义异常处理,可以调用execRequest进行单个请求逐个执行。

以特殊事件强绑接口为例,使用请求拆分功能后紧接着实际发送请求的示例代码为:

MultiUserRequestExecutor executor = MultiUserRequestExecutorImpl.getInstance();List<RequestExecutionContext<EditForceMatchedOrderRequest, EditForceMatchedOrderResponse>> execResults =
                executor.execRequests(
                        // 拆分后的请求列表
                        splitRequests,
                        // 请求的响应契约类型
                        EditForceMatchedOrderResponse.class,
                        // 请求的客户端实例
                        FlightchangeSpecialeventServiceClient.getInstance(),
                        // 请求的对应操作名
                        "editForceMatchedOrder");
1
2
3
4
5
6
7
8
9
10
11
12

返回值中的RequestExecutionContext对象包括了请求,响应,SwitchTag以及该次请求的异常信息,一般来说无需关心。

# 请求聚合

SDK中提供了一些标准的api来对拆分后不同用户的多个请求的一系列响应做聚合,最终返回客户端的只有一个响应对象,使得业务代码中除了调用部分之外的代码可以保持一致。

响应聚合的方式主要包括以下三类:根据UCS策略聚合

P aggregateByUCS(List<RequestExecutionContext<R,P>> responseContext,
                 // 可以不传,则默认有响应都是成功,不进行过滤
                 Predicate<P> failedRespPredictor,
                 String itemCollectionFieldName,
                 Function<T, K> splitKeyGetter,
                 MappingFieldType keyType) throws Exception;
1
2
3
4
5
6

场景:广播请求后返回了多个区域的多个用户的请求,需要筛选出Region中灰度范围内用户的数据,保证数据新鲜度。

其中,responseContext为上述请求执行后返回的包装结果,failedRespPredictor为判断单个响应是否成功的函数,其余参数和请求拆分中的定义保持一致(用户信息对象为响应中的对象)。

WARNING

注意:返回的响应集合中,如果有一个响应经过failedRespPredictor判断为失败,则默认情况下,会认为整个请求失败,返回该失败的响应。该行为可以通过配置ignoreFailureAndExceptions改变,后续配置项介绍会详细说明。

示例代码:以用规则ID查询所有匹配的强绑规则订单为例,该场景下请求内仅含有需要查询的规则ID无用户信息,所以被广播到了SHASIN两个Region同时进行查询。现在需要对查询结果做聚合:

MultiUserResponseAggregator aggregator = MultiUserResponseAggregatorImpl.getInstance();
QueryForceMatchedOrderResponse aggregated = aggregator.aggregateByUCS(execResults,
        response -> response.getResponseStatus().getAck() != AckCodeType.Success,
        "forceMatchedOrderList",
        ForceMatchedOrder::getOrderId,
        MappingFieldType.FLIGHT_ORDER_ID);
// handle response as used to be
1
2
3
4
5
6
7

聚合全量不同的结果

P aggregateAllDistinct(List<RequestExecutionContext<R,P>> responseContext,
                       String itemCollectionFieldName,
                       // 判断两个含有用户信息的对象是否属于同一个业务记录的函数,默认为Object.equals
                       BiPredicate<T, T> itemEqualPredictor,
                       // 可以不传,则默认有响应都是成功,不进行过滤
                       Predicate<P> failedRespPredictor) throws Exception;
1
2
3
4
5
6

场景:批量操作请求按照用户被拆分成多个后,需要获取所有响应进行展示,或数据完全隔离后单边进行查询。

示例场景:以特殊事件强绑接口为例,请求按照用户被拆分成多个请求后,返回的响应是操作失败的订单列表,此时只需要聚合所有失败的订单返回给客户端即可。示例代码如下:

MultiUserResponseAggregator aggregator = MultiUserResponseAggregatorImpl.getInstance();
EditForceMatchedOrderResponse response = aggregator.aggregateAllDistinct(
        execResults,
        "updateFailedList",
        // 返回的itemCollection为Long,直接使用默认的Object.equals比较即可
        null,
        // 无特别的响应状态码,默认即可
        null);
1
2
3
4
5
6
7
8

返回任意结果(任意成功/任意失败/失败优先)

// 任意成功
P getAnySuccessResponse(List<RequestExecutionContext<R,P>> responseContext, Predicate<P> successRespPredictor);// 失败优先
<R extends SpecificRecord, P extends SpecificRecord>
P getAnyResponseWithFailFast(List<RequestExecutionContext<R,P>> responseContext,
                             Predicate<P> failedRespPredictor) throws Exception;// 所有失败
<R extends SpecificRecord, P extends SpecificRecord>
List<RequestExecutionContext<R,P>> getAllFailedResponseContext(
        List<RequestExecutionContext<R,P>> responseContext, Predicate<P> failedRespPredictor);
1
2
3
4
5
6
7
8
9
10
11
12

场景:批量操作请求按照用户被拆分成多个后,响应中不含有用户信息,仅存在成功/失败/状态码的字段

典型场景示例代码:综合以上的用法,我们针对典型的场景给出两套标准的示例代码:
【1】批量编辑强绑订单,请求中含有多个待编辑的订单信息,响应为编辑失败的订单号集合

private EditForceMatchedOrderResponse editForceMatchedOrder(EditForceMatchedOrderRequest request) {MultiUserRequestSplitter splitter = MultiUserRequestSplitterImpl.getInstance();
    MultiUserRequestExecutor executor = MultiUserRequestExecutorImpl.getInstance();
    MultiUserResponseAggregator aggregator = MultiUserResponseAggregatorImpl.getInstance();try {
        Map<SwitchTag, EditForceMatchedOrderRequest> splitRequests =splitter.split(
          request,
          "forceMatchedOrderList",
          ForceMatchedOrder::getOrderId,
          MappingFieldType.FLIGHT_ORDER_ID);List<RequestExecutionContext<EditForceMatchedOrderRequest, EditForceMatchedOrderResponse>> execResults = executor.execRequests(
          splitRequests,
          EditForceMatchedOrderResponse.class,
          FlightchangeSpecialeventServiceClient.getInstance(),
          "editForceMatchedOrder");return aggregator.aggregateAllDistinct(execResults, "updateFailedList", null, null);
    } catch (Exception e) {
        // exception process
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

【2】根据强绑规则ID查询所有匹配的订单信息,请求中只含有规则ID,响应为所有匹配的订单信息的集合

private QueryForceMatchedOrderResponse queryForceMatchedOrder(QueryForceMatchedOrderRequest request) {
    MultiUserRequestSplitter splitter = MultiUserRequestSplitterImpl.getInstance();
    MultiUserRequestExecutor executor = MultiUserRequestExecutorImpl.getInstance();
    MultiUserResponseAggregator aggregator = MultiUserResponseAggregatorImpl.getInstance();try {
        Map<SwitchTag, QueryForceMatchedOrderRequest> splitRequests = splitter.broadcast(request);List<RequestExecutionContext<QueryForceMatchedOrderRequest, QueryForceMatchedOrderResponse>> execResults = executor.execRequests(
          splitRequests,
          QueryForceMatchedOrderResponse.class,
          FlightchangeSpecialeventServiceClient.getInstance(),
          "queryForceMatchedOrder");return aggregator.aggregateByUCS(execResults,
                                         "forceMatchedOrderList",
                                         ForceMatchedOrder::getOrderId,
                                         MappingFieldType.FLIGHT_ORDER_ID);
    } catch (Exception e) {
        // exception process
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 配置项列表

为了启用SDK中的多用户请求处理功能,开发者必须在客户端的QConfig上新建名为requestprocessorconfig.json的配置文件, 并按照目标操作的维度配置必要的信息。示例的配置文件如下:

[
    {
        "requestTypeFullName" : "com.huwei.soa.flight.flightchange.specialevent.v1.EditForceMatchedOrderRequest", // 要调用的操作的请求契约全类名
        "targetServiceCode" : "24249",  // 要调用的服务对应的serviceCode,用于关联UCS策略以及路由规则
        "splitterSettings" : {
            "enableRequestSplit" : true,  // 是否打开请求拆分功能,默认不打开,即原样转发请求
            "splitMode" : "BY_UID",  // 拆分的模式
            "interruptOnUDLError" : false, // 查询UDL信息出现异常如超时时,是否直接中断当前请求。默认或设置为false时,查询UDL出错,请求会继续被执行,但是不会带上UDL信息,所以都会被路由到SHA。设置为true时,查询UDL出错,方法直接抛错中断执行
            "allowNullSplitKey": false // 默认情况下,split key为空的时候走SHA。设置为true后,split key为空的时候,该key会拆分为广播的请求。场景为某些特殊的批量查询下,查询的key即可能是订单号也有可能是规则ID。
        },
        "executorSettings" : {
            "enableConcurrentExecute" : false, // 是否启用并发请求。当拆分后的用户数量很多,或客户端对响应时间比较敏感的情况下,该选项设置为true可以开启并发执行。默认为不开启。
            "concurrentExecThreshold" : 10,  // 并发执行的请求个数阈值。默认为0。当并发请求开启后,可以通过设置该值,来达到仅当拆分后请求数量大于该阈值才并发执行的效果。
            "maxConcurrentThreads" : 16, // 最大并发线程数,仅对当前操作生效。
            "interruptOnRemoteCallError" : false, // 是否在远程调用发生异常时立即中断。默认为不中断。
            "execTimeoutMillis" : 3000, // 并发执行时,总体的超时时间(单位ms)。默认为10秒。
            "requestFormat" : "json" // 调用服务端时的请求格式,针对服务端只接受特定的格式的场景。默认即跟随baiji框架设置。
        },
        "aggregatorSettings" : {
            "ignoreFailureAndExceptions" : false, // 是否在聚合时忽略异常和失败的请求,默认为不忽略。设置为true时,异常或失败的请求会被跳过,系统只会聚合合法的响应并返回客户端。
            "dataInconsistentErrorLogLevel" : "INFO", // 当Region之间数据不一致时,log信息的级别。可选有INFO, ERROR, DISABLE
            "disableUCSFiltering" : false // 在数据完全隔离后,跳过UCS过滤的步骤,直接聚合全量数据。
        }
    },
  ...
]
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

splitMode:拆分的模式
【1】BY_UID:默认的每个用户拆分一个请求,适用于绝大部分情况;
【2】BY_UDL:(使用前请联系上云组评估)仅当单个批量请求的用户可能非常多导致性能问题时使用;
【3】BROADCAST: 广播模式,同一个请求复制到所有Region

(adsbygoogle = window.adsbygoogle || []).push({});