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

# 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

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

特点:1)应用程序间通信的中间层;2)轻量级网络代理;3)应用程序无感知;4)解耦应用程序的重试/超时、监控、追踪和服务发现;
Service Mesh
对于研发的好处:
【1】功能增强:Service Mesh
中的功能比现有的SOA
更多,未来功能迭代也会更快;
【2】语言互通:无论是Java
调用小众语言服务还是小众语言调用Java
服务,使用的体验一致,互调会更方便;
【3】升级无感:Service Mesh Sidecar
的升级不需要业务介入,发布系统就可以完成;
Service Mesh
对于Java
使用者:无缝过渡:不改变开发体验,继续沿用现有SOA SDK
,SOA SDK
底层会做简化,减少发布频率;
对多语言使用者的好处:
【1】体验升级:可以得到几乎和Java
一样SOA
开发体验;
【2】性能体验:不需要WebAPI
,也不会局限HTTP
。对性能有更高要求的还可以使用gRPC
和gRPC Streaming
来提升性能。
【3】监控提升:不再强依赖CAT
,使用开源产品上报数据也可以和CAT
做整合。

接入Service Mesh
后一些行为变化:
功能点 | 接入前 | 接入后 |
---|---|---|
谁负责注册服务实例 | SOA SDK | SOA Operator |
谁负责执行路由 | SOA SDK | Sidecar |
谁负责执行负载均衡 | SOA SDK | Sidecar |
谁负责执行治理功能(黑白名单、限流、熔断) | SOA SDK | Sidecar |
# 四、服务网关的路由机制
# SLB
SLB(Software Load Balancer)
软负载均衡,主要用于为使用HTTP
协议的请求提供路由和负载均衡功能。

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
。支持反爬的Gateway
:H5 Gateway
/TCP Gateway
/PCI Gateway
。生效粒度:应用粒度和SOA
服务粒度。反爬操作:黑名单(拒绝请求并返回错误信息)灰名单(要求填写验证码校验)标记并转发。
【3】访问控制: 1)IP白名单,适用的网关:Affiliation Gateway
和Corp Gateway
; 2)以服务粒度配置;
【4】基于Token
的验证机制:1)支持的网关Affiliation Gateway
; 2)以服务粒度配置;3)通过AppKey
+AppSecret
动态获取当前Token
;
# 五、SLB 与 Gateway 的区别
SLB | Gateway |
---|---|
核心功能是路由和负载均衡 | 核心功能是路由 |
转发目标是一组服务器 | 转发目标是一个URL |
基本不涉及任何业务逻辑 | 内含一些业务逻辑:跨域请求处理/用户认证与授权/请求加解密/请求协议转换 |
只做同协议请求转发 |
# 六、WebApi
WebApi
为SOA
服务提供了可通过域名访问的访问入口:
【1】路由功能与SDK
基本对齐;
【2】按需申请并开放访问;
主要应用场景:Gateway
访问;
副作用:1)配置同步链路较长;2)运维成本高;

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

# 基于 UDL 的流量调度
UDL(User Data Location)
是用户数据的归属地,取自用户注册海外账号时站点的区域(Locale)设置后两位。便于维护和数据合规。下发规则:仅对海外用户下发,用户登录时或登录后启动应用时下发,下发后保存在客户端本地。
特点:用户粘滞、百分之一灰度、支持白名单。
输入: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对应的类型
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
【2】广播请求至所有Region
: 场景:请求中不含有用户信息,但是返回结果会存在多个用户的数据。例如最终行程匹单,利用规则ID
查询所有匹配特殊事件规则的订单。
Map<SwitchTag, R> broadcast(R originalRequest) throws RequestSplitException;
用户只需要提供原始的请求,该方法就会将该请求复制多份到每个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
}
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;
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");
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;
2
3
4
5
6
场景:广播请求后返回了多个区域的多个用户的请求,需要筛选出Region
中灰度范围内用户的数据,保证数据新鲜度。
其中,responseContext
为上述请求执行后返回的包装结果,failedRespPredictor
为判断单个响应是否成功的函数,其余参数和请求拆分中的定义保持一致(用户信息对象为响应中的对象)。
WARNING
注意:返回的响应集合中,如果有一个响应经过failedRespPredictor
判断为失败,则默认情况下,会认为整个请求失败,返回该失败的响应。该行为可以通过配置ignoreFailureAndExceptions
改变,后续配置项介绍会详细说明。
示例代码:以用规则ID
查询所有匹配的强绑规则订单为例,该场景下请求内仅含有需要查询的规则ID
无用户信息,所以被广播到了SHA
和SIN
两个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
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;
2
3
4
5
6
场景:批量操作请求按照用户被拆分成多个后,需要获取所有响应进行展示,或数据完全隔离后单边进行查询。
示例场景:以特殊事件强绑接口为例,请求按照用户被拆分成多个请求后,返回的响应是操作失败的订单列表,此时只需要聚合所有失败的订单返回给客户端即可。示例代码如下:
MultiUserResponseAggregator aggregator = MultiUserResponseAggregatorImpl.getInstance();
EditForceMatchedOrderResponse response = aggregator.aggregateAllDistinct(
execResults,
"updateFailedList",
// 返回的itemCollection为Long,直接使用默认的Object.equals比较即可
null,
// 无特别的响应状态码,默认即可
null);
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);
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
}
}
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
}
}
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过滤的步骤,直接聚合全量数据。
}
},
...
]
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
;
← 七、QMQ 上云方案 九、文件系统 →