一、背景介绍
网关作为微服务生态中的重要一环,因为历史起因,中间件团队没有对立的微服务API网关,为此筹备技术预研打造一个功能齐全、可用性高的业务网关。
二、技术选型
常见的开源网关依照语言分类有如下几类:
- Nginx+Lua:OpenResty、Kong 等;
- Java:Zuul1/Zuul2、Spring Cloud Gateway、gravitee-gateway、Dromara Soul 等;
- Go:janus、GoKu API Gateway 等;
- Node.js:Express Gateway、MicroGateway 等。
因为团队内成员基本上为Java技术栈,因而并不打算深入研究非Java语言的网关。接下来咱们次要调研了Zuul1、Zuul2、Spring Cloud Gateway、Dromara Soul。
业界支流的网关基本上能够分为上面三种:
- Servlet + 线程池
- NIO(Tomcat / Jetty) + Servlet 3.0 异步
- NettyServer + NettyClient
在进行技术选型的时候,次要思考功能丰富度、性能、稳定性。在重复比照之后,决定抉择基于Netty框架进行网关开发;然而思考到工夫的紧迫性,最终抉择为针对 Zuul2 进行定制化开发,在 Zuul2 的代码骨架之下来欠缺网关的整个体系。
三、Zuul2 介绍
接下来咱们简要介绍一下 Zuul2 要害知识点。
Zuul2 的架构图:
为了解释下面这张图,接下来会别离介绍几个点
- 如何解析 HTTP 协定
- Zuul2 的数据流转
- 两个责任链:Netty ChannelPipeline责任链 + Filter责任链
3.1 如何解析 HTTP 协定
学习Zuul2须要肯定的铺垫常识,比方:Google Guice、RxJava、Netflix archaius等,然而更要害的应该是:如何解析HTTP协定,会影响到后续Filter责任链的原理解析,为此先剖析这个关键点。
首先咱们介绍官网文档中的一段话:
By default Zuul doesn't buffer body content, meaning it streams the received headers to the origin before the body has been received.This streaming behavior is very efficient and desirable, as long as your filter logic depends on header data.
翻译成中文:
默认状况下Zuul2并不会缓存申请体,也就意味着它可能会先发送接管到的申请Headers到后端服务,之后接管到申请体再持续发送到后端服务,发送申请体的时候,也不是组装为一个残缺数据之后才发,而是接管到一部分,就转发一部分。这个流式行为是高效的,只有Filter过滤的时候只依赖Headers的数据进行逻辑解决,而不须要解析RequestBody。
下面这段话映射到Netty Handler中,则意味着Zuul2并没有应用HttpObjectAggregator。
咱们先看一下惯例的Netty Server解决HTTP协定的样例:
NettyServer样例
@Slf4jpublic class ConfigServerBootstrap { public static final int WORKER_THREAD_COUNT = Runtime.getRuntime().availableProcessors(); public void start(){ int port = 8080; EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(WORKER_THREAD_COUNT); final BizServerHandler bizServerHandler = new BizServerHandler(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new IdleStateHandler(10, 10, 0)); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new HttpObjectAggregator(500 * 1024 * 1024)); pipeline.addLast(bizServerHandler); } }); log.info("start netty server, port:{}", port); serverBootstrap.bind(port).sync(); } catch (InterruptedException e) { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); log.error(String.format("start netty server error, port:%s", port), e); } }}
这个例子中的两个要害类为:HttpServerCodec、HttpObjectAggregator。
HttpServerCodec是HttpRequestDecoder、HttpResponseEncoder的组合器。
- HttpRequestDecoder职责:将输出的ByteBuf解析成HttpRequest、HttpContent对象。
- HttpResponseEncoder职责:将HttpResponse、HttpContent对象转换为ByteBuf,进行网络二进制流的输入。
HttpObjectAggregator的作用:组装HttpMessage、HttpContent为一个残缺的FullHttpRequest或者FullHttpResponse。
当你不想关怀chunked分块传输的时候,应用HttpObjectAggregator是十分有用的。
HTTP协定通常应用Content-Length来标识body的长度,在服务器端,须要先申请对应长度的buffer,而后再赋值。如果须要一边生产数据一边发送数据,就须要应用"Transfer-Encoding: chunked" 来代替Content-Length,也就是对数据进行分块传输。
接下来咱们看一下Zuul2为了解析HTTP协定做了哪些解决。
Zuul的源码:https://github.com/Netflix/zuul,基于v2.1.5。
// com.netflix.zuul.netty.server.BaseZuulChannelInitializer#addHttp1Handlersprotected void addHttp1Handlers(ChannelPipeline pipeline) { pipeline.addLast(HTTP_CODEC_HANDLER_NAME, createHttpServerCodec()); pipeline.addLast(new Http1ConnectionCloseHandler(connCloseDelay)); pipeline.addLast("conn_expiry_handler", new Http1ConnectionExpiryHandler(maxRequestsPerConnection, maxRequestsPerConnectionInBrownout, connectionExpiry));}
// com.netflix.zuul.netty.server.BaseZuulChannelInitializer#createHttpServerCodecprotected HttpServerCodec createHttpServerCodec() { return new HttpServerCodec( MAX_INITIAL_LINE_LENGTH.get(), MAX_HEADER_SIZE.get(), MAX_CHUNK_SIZE.get(), false );}
通过比照下面的样例发现,Zuul2并没有增加HttpObjectAggregator,也就是须要自行去解决chunked分块传输问题、自行组装申请体数据。
为了解决下面说的chunked分块传输问题,Zuul2通过判断是否LastHttpContent,来判断是否接管实现。
3.2 Zuul2 数据流转
如上图所示,Netty自带的HttpServerCodec会将网络二进制流转换为Netty的HttpRequest对象,再通过ClientRequestReceiver编解码器将HttpRequest转换为Zuul的申请对象HttpRequestMessageImpl;
申请体RequestBody在Netty自带的HttpServerCodec中被映射为HttpContent对象,ClientRequestReceiver编解码器顺次接管HttpContent对象。
实现了上述数据的转换之后,就流转到了最重要的编解码ZuulFilterChainHandler,外面会执行Filter链,也会发动网络申请到真正的后端服务,这一切都是在ZuulFilterChainHandler中实现的。
失去了后端服务的响应后果之后,也通过了Outbound Filter的过滤,接下来就是通过ClientResponseWriter把Zuul自定义的响应对象HttpResponseMessageImpl转换为Netty的HttpResponse对象,而后通过HttpServerCodec转换为ByteBuf对象,发送网络二进制流,实现响应后果的输入。
这里须要特地阐明的是:因为Zuul2默认不组装一个残缺的申请对象/响应对象,所以Zuul2是别离针对申请头+申请Headers、申请体进行Filter过滤拦挡的,也就是说对于申请,会走两遍前置Filter链,对于响应后果,也是会走两遍后置Filter链拦挡。
3.3 两个责任链
3.3.1 Netty ChannelPipeline责任链
Netty的ChannelPipeline设计,通过往ChannelPipeline中动静增减Handler进行定制扩大。
接下来看一下Zuul2 Netty Server中的pipeline有哪些Handler?
接着持续看一下Zuul2 Netty Client的Handler有哪些?
本文不针对具体的Handler进行具体解释,次要是给大家一个整体的视图。
3.3.2 Filter责任链
申请发送到Netty Server中,先进行Inbound Filters的拦挡解决,接着会调用Endpoint Filter,这里默认为ProxyEndPoint(外面封装了Netty Client),发送申请到实在后端服务,获取到响应后果之后,再执行Outbound Filters,最终返回响应后果。
三种类型的Filter之间是通过nextStage属性来连接的。
Zuul2存在一个定时工作线程GroovyFilterFileManagerPoller,定期扫描特定的目录,通过比对文件的更新工夫戳,来判断是否发生变化,如果有变动,则从新编译并放入到内存中。
通过定位工作实现了Filter的动静加载。
四、性能介绍
下面介绍了Zuul2的局部知识点,接下来介绍网关的整体性能。
4.1 服务注册发现
网关承当了申请转发的性能,须要肯定的办法用于动静发现后端服务的机器列表。
这里提供两种形式进行服务的注册发现:
集成网关SDK
- 网关SDK会在服务启动之后,监听ContextRefreshedEvent事件,被动操作zk注销信息到zookeeper注册核心,这样网关服务、网关治理后盾就能够订阅节点信息。
- 网关SDK增加了ShutdownHook,在服务下线的时候,会删除注销在zk的节点信息,用于告诉网关服务、网关治理后盾,节点已下线。
手工配置服务的机器节点信息
- 在网关治理后盾,手工增加、删除机器节点。
- 在网关治理后盾,手工设置节点上线、节点下线操。
为了避免zookeeper故障,网关治理后盾已提供HTTP接口用于注册、勾销注册作为兜底措施。
4.2 动静路由
动静路由分为:机房就近路由、灰度路由(相似于Dubbo的标签路由性能)。
- 机房就近路由:申请最好是不要跨机房,比方申请打到网关服务的X机房,那么也应该是将申请转发给X机房的后端服务节点,如果后端服务不存在X机房的节点,则申请到其余机房的节点。
- 灰度路由:相似于Dubbo的标签路由性能,如果心愿对后端服务节点进行分组隔离,则须要给后端服务一个标签名,建设"标签名→节点列表"的映射关系,申请方携带这个标签名,申请到相应的后端服务节点。
网关治理后盾反对动静配置路由信息,动静开启/敞开路由性能。
4.3 负载平衡
以后反对的负载平衡策略:加权随机算法、加权轮询算法、一致性哈希算法。
能够通过网关治理后盾动静调整负载平衡策略,反对API接口级别、利用级别的配置。
负载平衡机制并未采纳Netflix Ribbon,而是仿造Dubbo负载平衡的算法实现的。
4.4 动静配置
API网关反对一套自洽的动静配置性能,在不依赖第三方配置核心的条件下,依然反对实时调整配置项,并且配置项分为全局配置、利用级别治理配置、API接口级别治理配置。
在自洽的动静配置性能之外,网关服务也与公司级别的配置核心进行买通,反对公司级配置核心配置相应的配置项。
4.5 API治理
API治理反对网关SDK主动扫描上报,也反对在治理后盾手工配置。
4.6 协定转换
后端的服务有很多是基于Dubbo框架的,网关服务反对HTTP→HTTP的申请转发,也反对HTTP→Dubbo的协定转换。
同时C++技术栈,采纳了tars框架,网关服务也反对HTTP → tras协定转换。
4.7 平安机制
API网关提供了IP黑白名单、OAuth认证受权、appKey&appSecret验签、矛盾加解密、vivo登录态校验的性能。
4.8 监控/告警
API网关通过对接通用监控上报申请访问信息,对API接口的QPS、申请响应吗、申请响应工夫等进行监控与告警;
通过对接根底监控,对网关服务本身节点进行CPU、IO、内存、网络连接等数据进行监控。
4.9 限流/熔断
API网关与限流熔断零碎进行买通,能够在限流熔断零碎进行API接口级别的配置,比方熔断配置、限流配置,而无需业务零碎再次对接限流熔断组件。
限流熔断零碎提供了对Netflix Hystrix、Alibaba Sentinel组件的封装。
4.10 无损公布
业务零碎的无损公布,这里分为两种场景介绍:
- 集成了网关SDK:网关SDK增加了ShutdownHook,会被动从zookeeper删除注销的节点信息,从而防止申请打到行将下线的节点。
- 未集成网关SDK:如果什么都不做,则只能依赖网关服务的心跳检测性能,会有15s的流量损失。庆幸的是治理后盾提供了流量摘除、流量复原的操作按钮,反对动静的上线、下线机器节点。
网关集群的无损公布:咱们思考了后端服务的无损公布,然而也须要思考网关节点本身的无损公布,这里咱们不再反复造轮子,间接应用的是CICD零碎的HTTP无损公布性能(Nginx动静摘除/上线节点)。
4.11 网关集群分组隔离
网关集群的分组隔离指的是业务与业务之间的申请应该是隔离的,不应该被局部业务申请打垮了网关服务,从而导致了别的业务申请无奈解决。
这里咱们会对接入网关的业务进行分组归类,不同的业务应用不同的分组,不同的网关分组,会部署独立的网关集群,从而隔离了危险,不必再放心业务之间的相互影响。
五、零碎架构
5.1 模块交互图
5.2 网关治理后盾
模块划分
5.3 通信机制
因为须要动静的下发配置,比方全局开关、利用级别的治理配置、接口级别的治理配置,就须要网关治理后盾能够与网关服务进行通信,比方推拉模式。
两种设计方案
- 基于注册核心的订阅告诉机制
- 基于HTTP的推模式 + 定时拉取
这里并未采纳第一种计划,次要是因为以下毛病:
- 重大依赖zk集群的稳定性
- 信息不私密(zk集群权限管控能力较弱、放心被误删)
- 无奈灰度下发配置,比方只对其中的一台网关服务节点配置失效
5.3.1 基于HTTP的推模式
因为Zuul2自身就自带了Netty Server,同理也能够再多启动一个Netty Server提供HTTP服务,让治理后盾发送HTTP申请到网关服务,进而发送配置数据到网关服务了。
所以图上的蓝色标记Netty Server用于接管客户端申请转发到后端节点,紫色标记Netty Server用于提供HTTP服务,接管配置数据。
5.3.2 全量配置拉取
网关服务在启动之初,须要发送HTTP申请到治理后盾拉取全副的配置数据,并且也须要拉取归属以后节点的灰度配置(只对这个节点失效的试验性配置)。
5.3.3 增量配置定时拉取
下面提到了"基于HTTP的推模式"进行配置的动静推送,也介绍了全局配置拉取,为了保险起见,网关服务还是新增了一个定时工作,用于定时拉取增量配置。
能够了解为兜底操作,就好比配置核心反对长轮询获取数据实时变更+定时工作获取全副数据。
在拉取到增量配置之后,会比对内存中的配置数据是否统一,如果统一,则不操作间接抛弃。
5.3.4 灰度配置下发
下面也提到了"灰度配置"这个词,这里具体解释一下什么是灰度配置?
比方当编辑了某个接口的限流信息,心愿在某个网关节点运行一段时间,如果没有问题,则调整配置让全副的网关服务节点失效,如果有问题,则也只是其中一个网关节点的申请流量出问题。
这样能够升高出错的概率,当某个比拟大的改变或者版本上线的时候,能够管制灰度部署一台机器,同时配置也只灰度到这台机器,这样危险就升高了很多。
灰度配置:能够了解为只在某些网关节点失效的配置。
灰度配置下发其实也是通过"5.3.1基于HTTP的推模式"来进行下发的。
5.4 网关SDK
网关SDK旨在实现后端服务节点的注册与下线、API接口列表数据上报,通过接入网关SDK即可缩小手工操作。网关SDK通过 ZooKeeper client操作节点的注册与下线,通过发动HTTP申请进行API接口数据的上报。
反对SpringMVC、SpringBoot的web接口主动扫描、Dubbo新老版本的Service接口扫描。
Dubbo 接口上报:
- 旧版Dubbo:自定义BeanPostProcessor,用于提取到ServiceBean,放入线程池异步上报到网关后盾。
- 新版Dubbo:自定义ApplicationListener,用于监听ServiceBeanExportedEvent事件,提取event信息,上报到网关后盾。
HTTP 接口上报:
- 自定义BeanPostProcessor,用于提取到Controller、RestController的RequestMapping注解,放入线程池异步上报API信息。
六、革新之路
6.1 动静配置
关联知识点:
- https://github.com/apache/commons-configuration
- https://github.com/Netflix/archaius
Zuul2依赖的动静配置为archaius,通过扩大ConcurrentMapConfiguration增加到ConcurrentCompositeConfiguration中。
新增GatewayConfigConfiguration,用于存储全局配置、治理配置、节点信息、API数据等。
@Singletonpublic class GatewayConfigConfiguration extends ConcurrentMapConfiguration { public GatewayConfigConfiguration() { /** * 设置这个值为true,才能够防止archaius强行去除value的类型,导致获取报错 * see com.netflix.config.ConcurrentMapConfiguration#setPropertyImpl(java.lang.String, java.lang.Object) */ this.setDelimiterParsingDisabled(Boolean.TRUE); } }
通过Google Guice管制Bean的加载程序,在较早的机会,执行ConfigurationManager.getConfigInstance(),获取到ConcurrentCompositeConfiguration,实现GatewayConfigConfiguration的初始化,而后再插入到第一个地位。
后续只须要对GatewayConfigConfiguration进行配置的增删查改操作即可。
6.2 路由机制
路由机制也是仿造的Dubbo路由机制,灰度路由是仿造的Dubbo的标签路由,就近路由能够了解为同机房路由。
申请处理过程:
客户端申请过去的时候,网关服务会通过path前缀提取到对应的后端服务名或者在申请Header中指定传递对应的serviceName,而后只在匹配到的后端服务中,持续API匹配操作,如果匹配到API,则筛选出对应的后端机器列表,而后进行路由、负载平衡,最终选中一台机器,将申请转发过来。
这里会有个疑难,如果不心愿只在某个后端服务中进行申请路由匹配,是心愿在一堆后端服务中进行匹配,须要怎么操作?
在前面的第七章节会解答这个疑难,请急躁浏览。
6.2.1 就近路由
当申请到网关服务,会提取网关服务本身的机房loc属性值,读取全局、利用级别的开关,如果就近路由开关关上,则筛选服务列表的时候,会过滤雷同loc的后端机器,负载平衡的时候,在雷同loc的机器列表中筛选一台进行申请。
如果没有雷同loc的后端机器,则降级从其余loc的后端机器中进行筛选。
其中loc信息就是机房信息,每个后端服务节点在SDK上报或者手工录入的时候,都会携带这个值。
6.2.2 灰度路由
灰度路由须要用户传递Header属性值,比方gray=canary_gray。
网关治理后盾配置灰度路由的时候,会建设grayName -> List<Server>映射关系,当网关治理后盾增量推送到网关服务之后,网关服务就能够通过grayName来提取配置下的后端机器列表,而后再进行负载平衡筛选机器。
如下图所示:
6.3 API映射匹配
网关在进行申请转发的时候,须要明确晓得申请哪一个服务的哪一个API,这个过程就是API匹配。
因为不同的后端服务可能会领有雷同门路的API,所以网关要求申请传递serviceName,serviceName能够搁置于申请Header或者申请参数中。
携带了serviceName之后,就能够在后端服务的API中去匹配了,有一些是相等匹配,有些是正则匹配,因为RESTFul协定,须要反对 /* 通配符匹配。
这里会有人疑难了,难道申请肯定须要显式传递serviceName吗?
为了解决这个问题,创立了一个gateway\_origin\_mapping表,用于path前缀或者域名前缀 映射到 serviceName,通过在治理后盾建设这个映射关系,而后推送到网关服务,即可解决显式传递serviceName的问题,会主动提取申请的path前缀、域名前缀,找到对应的serviceName。
如果不心愿是在一个后端服务中进行API匹配,则需浏览前面的第七章节。
6.4 负载平衡
替换 ribbon 组件,改为仿造 Dubbo 的负载平衡机制。
public interface ILoadBalance { /** * 从服务列表中筛选一台机器进行调用 * @param serverList * @param originName * @param requestMessage * @return */ DynamicServer select(List<DynamicServer> serverList, String originName, HttpRequestMessage requestMessage); }
替换的理由:ribbon的服务列表更新只是定期更新,如果不思考简单的筛选过滤,是满足要求的,然而如果想要灵便的依据申请头、申请参数进行筛选,ribbon则不太适宜。
6.5 心跳检测
外围思路:当网络申请失常返回的时候,心跳检测是不须要,此时后端服务节点必定是失常的,只须要定期检测未被申请的后端节点,超过肯定的谬误阈值,则标记为不可用,从机器列表中剔除。
第一期先实现简略版本:通过定时工作定期去异步调用心跳检测Url,如果超过失败阈值,则从从负载平衡列表中剔除。
异步申请采纳httpasyncclient组件解决。
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpasyncclient</artifactId> <version>4.1.4</version></dependency>
计划为:HealthCheckScheduledExecutor + HealthCheckTask + HttpAsyncClient。
6.6 日志异步化革新
Zuul2默认采纳的log4j进行日志打印,是同步阻塞操作,须要批改为异步化操作,改为应用logback的AsyncAppender。
日志打印也是影响性能的一个关键点,须要特地留神,后续会掂量是否切换为log4j2。
6.7 协定转换
HTTP -> HTTP
Zuul2采纳的是ProxyEndpoint用于反对HTTP -> HTTP协定转发。
通过Netty Client的形式发动网络申请到实在的后端服务。
HTTP -> Dubbo
采纳Dubbo的泛化调用实现HTTP -> Dubbo协定转发,能够采纳$invokeAsync。
HTTP → Tars
基于tars-java采纳相似于Dubbo的泛化调用的形式实现协定转发,基于https://github.com/TarsCloud/TarsGateway革新而来的。
6.8 无损公布
网关作为申请转发,当然心愿在业务后端机器部署的期间,不应该把申请转发到还未部署实现的节点。
业务后端机器节点的无损公布,这里分为两种场景介绍:
- 集成了网关SDK 网关SDK增加了ShutdownHook,会被动从zookeeper删除注销的节点信息,从而防止申请打到行将下线的节点。
- 未集成网关SDK 如果什么都不做,则只能依赖网关服务的心跳检测性能,会有15s的流量损失。庆幸的是治理后盾提供了流量摘除、流量复原的操作按钮,反对动静的上线、下线机器节点。
设计方案
咱们给后端机器节点dynamic\_forward\_server表新增了一个字段online,如果online=1,则代表在线,接管流量,反之,则代表下线,不接管流量。
网关服务gateway-server新增一个路由:OnlineRouter,从后端机器列表中筛选online=1的机器,过滤掉不在线的机器,则实现了无损公布的性能。
public interface IRouter { /** * 过滤 * @param serverList * @param originName * @param requestMessage * @return */ List<DynamicServer> route(List<DynamicServer> serverList, String originName, HttpRequestMessage requestMessage); }
6.9 网关集群分组隔离
网关集群的分组隔离指的是业务与业务之间的申请应该是隔离的,不应该被局部业务申请打垮了网关服务,从而导致了别的业务申请无奈解决。
这里咱们会对接接入网关的业务进行分组归类,不同的业务应用不同的分组,不同的网关分组,会部署独立的网关集群,从而隔离了危险,不必再放心业务之间的相互影响。
举例:
金融业务在生产环境存在一个灰度点检环境,为了配合金融业务的迁徙,这边也必须有一套独立的环境为之服务,那是否重新部署一套全新的零碎呢(独立的前端+独立的治理后盾+独立的网关集群)
其实不用这么操作,咱们只须要部署一套独立的网关集群即可,因为网关治理后盾,能够同时配置多个网关分组的数据。
创立一个新的网关分组finance-gray,而新的网关集群只须要拉取finance-gray分组的配置数据即可,不会对其余网关集群造成任何影响。
七、.如何疾速迁徙业务
在业务接入的时候,现有的网关呈现了一个难堪的问题,当某些业务方自行搭建了一套Spring Cloud Gateway网关,外面的服务没有清晰的path前缀、独立的域名拆分,尽管是微服务体系,然而大家共用一个域名,接口前缀也没有良好的划分,混用在一起。
这个时候如果再依照原有的申请解决流程,则须要业务方进行Nginx的大量批改,须要在location的中央都显式传递serviceName参数,然而业务方不违心进行这一个调整。
针对这个问题,其实实质起因在于申请匹配逻辑的不一致性,现有的网关是先匹配服务利用,再进行API匹配,这样效率高一些,而Spring Cloud Gateway则是先API匹配,命中了才晓得是哪个后端服务。
为了解决这个问题,网关再次建设了一个 "微服务集" → "微服务利用列表" 的映射关系,治理后盾反对这个映射关系的推送。
一个网关分组上面会有很多应用服务,这里能够拆分为子集合,能够了解为微服务集就是外面的子集合。
客户端申请传递过去的时候,须要在申请Header传递scTag 参数,scTag用来标记是哪个微服务集,而后提取到scTag对应的所有后端服务利用列表,顺次去对应的应用服务列表中进行API匹配,如果命中了,则代表申请转发到以后利用的后端节点,而对原有的架构革新很小。
如果不想改变客户端申请,则须要在业务域名的Nginx上进行调整,传递scTag申请Header。
作者:Lin Chengjun