• Spring Cloud zuul扩展能力设计和心得


    前言

    实际上Spring Cloud已经废弃zuul了,改用gateway,但是webflux的技术并没在实际项目大规模普及,还有很多servlet NIO的应用,所以zuul还是很有必要改造的,实测zuul调优(调节转发的连接池)跟gateway性能上差不多,所以研究了下zuul,发现设计理念很不错。

    核心理念

    zuul的原理,笔者的上一章已经大致说过,参考Spring Cloud zuul与CloseableHttpClient连接池,TLS证书认证_fenglllle的博客-CSDN博客

    说了连接池部分,包括是怎么关闭的,但是zuul的数据怎么传递的呢 

    zuul的核心理念实际上是线程变量的传递,threadlocal,所以如果需要子线程或者其他线程池,那么需要对传递的变量进行改造,使用

    InheritableThreadLocal

    简单翻译 

    请求上下文保存请求、响应、状态信息和数据,供ZuulFilters访问和共享。RequestContext在请求的持续时间内存在,并且是ThreadLocal。
    可以通过设置contextClass来替换RequestContext的扩展。这里大多数方法都是方便扩展的方法; RequestContext是ConcurrentHashMap的扩展(继承)

     如果自定义线程池,那么需要InheritableThreadLocal,实际上Spring和日志框架都是定制的,threadlocal需要注意自己管理生命周期,线程结束必须clear,否则会造成内存泄漏,有点C++编程的思维。

    扩展route

    zuul提供全局配置和每个route的配置,全局配置根据自己需要定制,但是全局生效,适合明确的需求,相对而言扩展自定义route更加灵活,自定义route的解析

    org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter

    在pre的filter中解析,SpringCloud的zuul starter自带

    只需要对org.springframework.cloud.netflix.zuul.filters.Route和org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute加入自定义的属性即可,Spring cloud会自定载入属性

    这种实现思路来源于8. Router and Filter: Zuul

    Spring cloud官方文档,Spring cloud对于Cookies and Sensitive Headers 的设计

    1. zuul:
    2. routes:
    3. users:
    4. path: /myusers/**
    5. sensitiveHeaders: Cookie,Set-Cookie,Authorization
    6. url: https://downstream

    扩展filter

    扩展filter,实际上非常简单,继承zuulfilter即可,但是建议所有的filter放在PreDecorationFilter之后,PreDecorationFilter的order为5,order越小优先级越高

    因为PreDecorationFilter有很多前置的条件判断,不过使用if else判断的,而且route的解析也在这里

    1. public Object run() {
    2. //上下文设计,threadlocal,后面的结果设计就是这个传递的数据回写的
    3. RequestContext ctx = RequestContext.getCurrentContext();
    4. final String requestURI = this.urlPathHelper
    5. .getPathWithinApplication(ctx.getRequest());
    6. if (insecurePath(requestURI)) {
    7. throw new InsecureRequestPathException(requestURI);
    8. }
    9. //解析route,刚刚写的自定义属性可以在这里使用,同时SpringCloud的敏感header也是这里配置的
    10. Route route = this.routeLocator.getMatchingRoute(requestURI);
    11. if (route != null) {
    12. String location = route.getLocation(); //目标地址
    13. //这里的设计理念,根据关键字匹配,类似重定向 转发等等
    14. if (location != null) {
    15. ctx.put(REQUEST_URI_KEY, route.getPath());
    16. ctx.put(PROXY_KEY, route.getId());
    17. if (!route.isCustomSensitiveHeaders()) {
    18. this.proxyRequestHelper.addIgnoredHeaders(
    19. this.properties.getSensitiveHeaders().toArray(new String[0]));
    20. }
    21. else {
    22. this.proxyRequestHelper.addIgnoredHeaders(
    23. route.getSensitiveHeaders().toArray(new String[0]));
    24. }
    25. if (route.getRetryable() != null) {
    26. ctx.put(RETRYABLE_KEY, route.getRetryable());
    27. }
    28. //HTTP转发,HTTPS同理
    29. if (location.startsWith(HTTP_SCHEME + ":")
    30. || location.startsWith(HTTPS_SCHEME + ":")) {
    31. ctx.setRouteHost(getUrl(location));
    32. ctx.addOriginResponseHeader(SERVICE_HEADER, location);
    33. }//forward转发,可以根据这个设计,设计mock能力
    34. else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
    35. ctx.set(FORWARD_TO_KEY,
    36. StringUtils.cleanPath(
    37. location.substring(FORWARD_LOCATION_PREFIX.length())
    38. + route.getPath()));
    39. ctx.setRouteHost(null);
    40. return null;
    41. }
    42. else {
    43. // set serviceId for use in filters.route.RibbonRequest
    44. ctx.set(SERVICE_ID_KEY, location);
    45. ctx.setRouteHost(null);
    46. ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
    47. }
    48. if (this.properties.isAddProxyHeaders()) {
    49. addProxyHeaders(ctx, route);
    50. String xforwardedfor = ctx.getRequest()
    51. .getHeader(X_FORWARDED_FOR_HEADER);
    52. String remoteAddr = ctx.getRequest().getRemoteAddr();
    53. if (xforwardedfor == null) {
    54. xforwardedfor = remoteAddr;
    55. }
    56. else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
    57. xforwardedfor += ", " + remoteAddr;
    58. }
    59. ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);
    60. }
    61. if (this.properties.isAddHostHeader()) {
    62. ctx.addZuulRequestHeader(HttpHeaders.HOST,
    63. toHostHeader(ctx.getRequest()));
    64. }
    65. }
    66. }
    67. else {
    68. log.warn("No route found for uri: " + requestURI);
    69. String forwardURI = getForwardUri(requestURI);
    70. ctx.set(FORWARD_TO_KEY, forwardURI);
    71. }
    72. return null;
    73. }

    比如可以根据forward转发的设计,设计mock能力,可以在数据库、配置中心等配置返回结果,也可以通过流量的录制,录制返回结果 ,比如bpf录制。

    mock response

    那么设计一个初步的mock response能力

    pre filter设计

    修改PreDecorationFilter,要是PreDecorationFilter提供扩展能力就好了,直接扩展,但是PreDecorationFilter写的if else,可以重构代码,把if else变成SPI,实现自定义扩展。

    简单写了,其中

    ctx.setRouteHost(null);

    极为关键,比如HTTP(S)转发的filter,就是根据Host判断的,但是Host就是一个信号量,如果明确信号量的概念,并且抽象,那么代码就更明晰了。

    post filter设计

    设计mock,需要根据现有的返回数据逻辑,可以看到post filter根据servlet response回写的方式

    那么这个response哪里初始化的呢,zuulrunner里面,通过包装类实现的

    手写route filter实现mock

    1. package org.springframework.cloud.netflix.zuul.filters.route;
    2. import com.netflix.zuul.ZuulFilter;
    3. import com.netflix.zuul.context.RequestContext;
    4. import com.netflix.zuul.exception.ZuulException;
    5. import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
    6. import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ROUTE_TYPE;
    7. public class MockHostRoutingFilter extends ZuulFilter {
    8. private ProxyRequestHelper helper;
    9. public MockHostRoutingFilter(ProxyRequestHelper helper) {
    10. this.helper = helper;
    11. }
    12. @Override
    13. public String filterType() {
    14. return ROUTE_TYPE;
    15. }
    16. @Override
    17. public int filterOrder() {
    18. return 0;//自定义
    19. }
    20. @Override
    21. public boolean shouldFilter() {
    22. //可以在PreDecorationFilter埋点设计
    23. //同时在@Bean,可以通过conditional on properties开启mock的bean
    24. return RequestContext.getCurrentContext().get("mock.path") != null;
    25. }
    26. @Override
    27. public Object run() throws ZuulException {
    28. String fullPath = RequestContext.getCurrentContext().get("mock.path") != null;
    29. // 自定义实现,取http code;body;headers
    30. return this.helper.setResponse(statusCode(),
    31. responseBody(),
    32. headers());
    33. }
    34. }

    总结

    SpringCloud实际上是链式设计,通过threadlocal连接数据,基于servlet逻辑,那么整个链路就可以自定义,实际上应该设计比较完善的filter,通过filter的基础上扩展,比如SPI的方式,可以提供各种能力比较方便,不过Spring Cloud zuul通过filter扩展也不错,但是需要threadlocal设置各种数据,尤其是标签数据,在

    PreDecorationFilter

    中,这个filter尤其重要,如果做成SPI模式就更好了。zuul相对简单,而且贴合servlet,如果使用gateway,那么需要使用webflux技术(netty异步能力),通信原理就是NIO和AIO的区别,数据传递就复杂很多,线程之间传递数据,使用

    InheritableThreadLocal

    不过,不需要太多的调优,使用角度会简单一些,但是定制化过程难度会增加,取舍而已,如果不定制,那么gateway是比较优的选择,如果追求极致性能,那么这2者都不建议,建议使用nginx+lua的方案,性能会强很多,而且可定制。

  • 相关阅读:
    下一代 SCA:流水线成分分析
    bootstrap学习(四)
    JS 流行框架(三):Koa2
    CSS篇十——(1)
    Java:实现将图像旋转90度算法(附完整源码)
    【Vue】style和class 列表渲染 使用v-for进行循环 监控失效 双向数据绑定 过滤案例 事件修饰符
    MQTT协议消息代理服务远程连接
    人工神经网络技术的优点,人工神经网络发展历程
    模型压缩-对模型结构进行优化
    elementui表单的验证问题
  • 原文地址:https://blog.csdn.net/fenglllle/article/details/133606084