Nepxion Discovery Agent

Stella981
• 阅读 929

前言

基于Spring Cloud的全链路灰度蓝绿发布功能,其中一个场景是,基于Header传递的全链路灰度路由,采用配置中心配置路由策略映射在网关或者服务上,支持根据用户自定义Header跟路由策略整合,最终转化为路由Header信息而实现,路由策略传递到全链路服务中。这是一个非常普遍的需求,但如果业务方用了服务之间异步调用的方式,会导致存储在ThreadLocal里的Header丢失的情况,导致全链路灰度蓝绿失效

方案调研

通过采用类似Hystrix线程池装饰方式来实现

方案比较简单,代码也不复杂,但对业务侵入非常明显,即凡是涉及到Java异步场景丢失线程上下文的场景中的线程都需要手工逐一去装饰。故而放弃

通过阿里巴巴的开源TTL来实现

查看了相关文档和源码,并咨询了作者,似乎仍旧难以满足笔者的需求。请参考如下链接:

https://github.com/alibaba/transmittable-thread-local/issues/171

同时根据官网上的性能压测数据,让笔者还是有点担心,TTL的Full GC次数每分钟是Java 标准Theadlocal的300多倍。请参考如下链接:

https://github.com/alibaba/transmittable-thread-local/blob/master/docs/performance-test.md

故而也放弃

通过Java Agent技术来实现

也许通过Java Agent字节码增强方式可以解决?笔者对Java Agent技术并不是非常有经验,故邀请了一个这方面的高手@zifeihan。经过几个月的努力,大功告成,即DiscoveryAgent在Nepxion官方Github开源

DiscoveryAgent

灰度路由Header和调用链Span在Hystrix线程池隔离模式下或者线程、线程池、@Async注解等异步调用Feign或者RestTemplate时,通过线程上下文切换会存在丢失Header的问题,通过下述步骤解决,同时适用于网关端和服务端。该方案可以替代Hystrix线程池隔离模式下的解决方案,也适用于其它有相同使用场景的基础框架和业务场景,例如:Dubbo

涵盖所有Java框架的异步场景,解决如下7个异步场景下丢失线程上下文的问题

  • @Async

  • Hystrix Thread Pool Isolation

  • Runnable

  • Callable

  • Single Thread

  • Thread Pool

  • SLF4J MDC

插件获取

插件使用

  • discovery-agent-starter- ${discovery.version}.jar为Agent引导启动程序,JVM启动时进行加载;discovery-agent/plugin目录包含discovery-agent-starter-plugin-strategy- ${discovery.version}.jar为Nepxion Discovery自带的实现方案,业务系统可以自定义plugin,解决业务自己定义的上下文跨线程传递

  • 通过如下-javaagent启动

-javaagent:/discovery-agent/discovery-agent-starter-${discovery.agent.version}.jar -Dthread.scan.packages=com.abc;com.xyz

参数说明

  • /discovery-agent:Agent所在的目录,需要对应到实际的目录上

  • -Dthread.scan.packages:Runnable,Callable对象所在的扫描目录,该目录下的Runnable,Callable对象都会被装饰。该目录最好精细和准确,这样可以减少被装饰的对象数,提高性能,目录如果有多个,用“;”分隔

  • -Dthread.request.decorator.enabled:异步调用场景下在服务端的Request请求的装饰,当主线程先于子线程执行完的时候,Request会被Destory,导致Header仍旧拿不到,开启装饰,就可以确保拿到。默认为开启,根据实践经验,大多数场景下,需要开启该开关

  • -Dthread.mdc.enabled:SLF4J MDC日志输出到异步子线程。默认关闭,如果需要,则开启该开关

扫描目录thread.scan.packages定义,该参数只作用于服务侧,网关侧不需要加 1. @Async场景下的扫描目录为org.springframework.aop.interceptor 2. Hystrix线程池隔离场景下的扫描目录为com.netflix.hystrix 3. 线程、线程池的扫描目录为自定义Runnable,Callable对象所在类的目录

参考指南示例中的异步服务启动参数。扫描目录中的三个包名,视具体场景按需配置

-javaagent:C:/opt/discovery-agent/discovery-agent-starter-${discovery.agent.version}.jar -Dthread.scan.packages=org.springframework.aop.interceptor;com.netflix.hystrix;com.nepxion.discovery.guide.service.feign

插件扩展

  • 根据规范开发一个插件,插件提供了钩子函数,在某个类被加载的时候,可以注册一个事件到线程上下文切换事件当中,实现业务自定义ThreadLocal的跨线程传递

  • plugin目录为放置需要在线程切换时进行ThreadLocal传递的自定义插件。业务自定义插件开发完后,放入到plugin目录下即可

具体步骤介绍,如下

① SDK侧工作

  • 新建ThreadLocal上下文类

`public class MyContext {
    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal() {
        @Override
        protected MyContext initialValue() {
            return new MyContext();
        }
    };

    public static MyContext getCurrentContext() {
        return THREAD_LOCAL.get();
    }

    public static void clearCurrentContext() {
        THREAD_LOCAL.remove();
    }

    private Map<String, String> attributes = new HashMap<>();

    public Map<String, String> getAttributes() {
        return attributes;
    }

    public void setAttributes(Map<String, String> attributes) {
        this.attributes = attributes;
    }
}
`

② Agent侧工作

  • 新建一个模块,引入如下依赖

<dependency>     <groupId>com.nepxion</groupId>     <artifactId>discovery-agent-starter</artifactId>     <version>${discovery.agent.version}</version>     <scope>provided</scope> </dependency>

  • 新建一个ThreadLocalHook类继承AbstractThreadLocalHook

`public class MyContextHook extends AbstractThreadLocalHook {
    @Override
    public Object create() {
        // 从主线程的ThreadLocal里获取并返回上下文对象
        return MyContext.getCurrentContext().getAttributes();
    }

    @Override
    public void before(Object object) {
        // 把create方法里获取到的上下文对象放置到子线程的ThreadLocal里
        if (object instanceof Map) {
            MyContext.getCurrentContext().setAttributes((Map<String, String>) object);
        }
    }

    @Override
    public void after() {
        // 线程结束,销毁上下文对象
        MyContext.clearCurrentContext();
    }
}
`

  • 新建一个Plugin类继承AbstractPlugin

`public class MyContextPlugin extends AbstractPlugin {
    private Boolean threadMyPluginEnabled = Boolean.valueOf(System.getProperty("thread.myplugin.enabled", "false"));

    @Override
    protected String getMatcherClassName() {
        // 返回存储ThreadLocal对象的类名,由于插件是可以插拔的,所以必须是字符串形式,不允许是显式引入类
        return "com.nepxion.discovery.guide.sdk.MyContext";
    }

    @Override
    protected String getHookClassName() {
        // 返回ThreadLocalHook类名
        return MyContextHook.class.getName();
    }

    @Override
    protected boolean isEnabled() {
        // 通过外部-Dthread.myplugin.enabled=true/false的运行参数来控制当前Plugin是否生效。该方法在父类中定义的返回值为true,即缺省为生效
        return threadMyPluginEnabled;
    }
}
`

  • 定义SPI扩展,在src/main/resources/META-INF/services目录下定义SPI文件

名称为固定如下格式

com.nepxion.discovery.agent.plugin.Plugin

内容为Plugin类的全路径

com.nepxion.discovery.guide.agent.MyContextPlugin

  • 执行Maven编译,把编译后的包放在discovery-agent/plugin目录下

  • 给服务增加启动参数并启动,如下

-javaagent:C:/opt/discovery-agent/discovery-agent-starter-${discovery.agent.version}.jar -Dthread.scan.packages=com.nepxion.discovery.guide.application -Dthread.myplugin.enabled=true

③ Application侧工作

  • 执行MyApplication,它模拟在主线程ThreadLocal放入Map数据,子线程通过DiscoveryAgent获取到该Map数据,并打印出来

`@SpringBootApplication
@RestController
public class MyApplication {
    private static final Logger LOG = LoggerFactory.getLogger(MyApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);

        invoke();
    }

    public static void invoke() {
        RestTemplate restTemplate = new RestTemplate();

        for (int i = 1; i <= 10; i++) {
            restTemplate.getForEntity("http://localhost:8080/index/" + i, String.class).getBody();
        }
    }

    @GetMapping("/index/{value}")
    public String index(@PathVariable(value = "value") String value) throws InterruptedException {
        Map<String, String> attributes = new HashMap<String, String>();
        attributes.put(value, "MyContext");

        MyContext.getCurrentContext().setAttributes(attributes);

        LOG.info("【主】线程ThreadLocal:{}", MyContext.getCurrentContext().getAttributes());

        new Thread(new Runnable() {
            @Override
            public void run() {
                LOG.info("【子】线程ThreadLocal:{}", MyContext.getCurrentContext().getAttributes());

                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                LOG.info("Sleep 5秒之后,【子】线程ThreadLocal:{} ", MyContext.getCurrentContext().getAttributes());
            }
        }).start();

        return "";
    }
}
`

输出结果,如下

2020-10-18 18:38:22.670  INFO 3780 --- [nio-8080-exec-1] c.n.d.guide.application.MyApplication    : 【主】线程ThreadLocal:{1=MyContext} 2020-10-18 18:38:22.738 INFO 3780 --- [       Thread-8] c.n.d.guide.application.MyApplication    : 【子】线程ThreadLocal:{1=MyContext} 2020-10-18 18:38:22.759 INFO 3780 --- [nio-8080-exec-2] c.n.d.guide.application.MyApplication    : 【主】线程ThreadLocal:{2=MyContext} 2020-10-18 18:38:22.760  INFO 3780 --- [       Thread-9] c.n.d.guide.application.MyApplication    : 【子】线程ThreadLocal:{2=MyContext} 2020-10-18 18:38:22.763 INFO 3780 --- [nio-8080-exec-3] c.n.d.guide.application.MyApplication    : 【主】线程ThreadLocal:{3=MyContext} 2020-10-18 18:38:22.764 INFO 3780 --- [      Thread-10] c.n.d.guide.application.MyApplication    : 【子】线程ThreadLocal:{3=MyContext} 2020-10-18 18:38:22.772 INFO 3780 --- [nio-8080-exec-4] c.n.d.guide.application.MyApplication    : 【主】线程ThreadLocal:{4=MyContext} 2020-10-18 18:38:22.773 INFO 3780 --- [      Thread-11] c.n.d.guide.application.MyApplication    : 【子】线程ThreadLocal:{4=MyContext} 2020-10-18 18:38:22.775 INFO 3780 --- [nio-8080-exec-5] c.n.d.guide.application.MyApplication    : 【主】线程ThreadLocal:{5=MyContext} 2020-10-18 18:38:22.776 INFO 3780 --- [      Thread-12] c.n.d.guide.application.MyApplication    : 【子】线程ThreadLocal:{5=MyContext} 2020-10-18 18:38:22.778 INFO 3780 --- [nio-8080-exec-6] c.n.d.guide.application.MyApplication    : 【主】线程ThreadLocal:{6=MyContext} 2020-10-18 18:38:22.779 INFO 3780 --- [      Thread-13] c.n.d.guide.application.MyApplication    : 【子】线程ThreadLocal:{6=MyContext} 2020-10-18 18:38:22.782 INFO 3780 --- [nio-8080-exec-7] c.n.d.guide.application.MyApplication    : 【主】线程ThreadLocal:{7=MyContext} 2020-10-18 18:38:22.783 INFO 3780 --- [      Thread-14] c.n.d.guide.application.MyApplication    : 【子】线程ThreadLocal:{7=MyContext} 2020-10-18 18:38:22.785 INFO 3780 --- [nio-8080-exec-8] c.n.d.guide.application.MyApplication    : 【主】线程ThreadLocal:{8=MyContext} 2020-10-18 18:38:22.786 INFO 3780 --- [      Thread-15] c.n.d.guide.application.MyApplication    : 【子】线程ThreadLocal:{8=MyContext} 2020-10-18 18:38:22.788 INFO 3780 --- [nio-8080-exec-9] c.n.d.guide.application.MyApplication    : 【主】线程ThreadLocal:{9=MyContext} 2020-10-18 18:38:22.789 INFO 3780 --- [      Thread-16] c.n.d.guide.application.MyApplication    : 【子】线程ThreadLocal:{9=MyContext} 2020-10-18 18:38:22.791 INFO 3780 --- [io-8080-exec-10] c.n.d.guide.application.MyApplication    : 【主】线程ThreadLocal:{10=MyContext} 2020-10-18 18:38:22.792 INFO 3780 --- [      Thread-17] c.n.d.guide.application.MyApplication    : 【子】线程ThreadLocal:{10=MyContext} 2020-10-18 18:38:27.738 INFO 3780 --- [       Thread-8] c.n.d.guide.application.MyApplication    : Sleep 5秒之后,【子】线程ThreadLocal:{1=MyContext}  2020-10-18 18:38:27.761 INFO 3780 --- [       Thread-9] c.n.d.guide.application.MyApplication    : Sleep 5秒之后,【子】线程ThreadLocal:{2=MyContext}  2020-10-18 18:38:27.764 INFO 3780 --- [      Thread-10] c.n.d.guide.application.MyApplication    : Sleep 5秒之后,【子】线程ThreadLocal:{3=MyContext}  2020-10-18 18:38:27.773 INFO 3780 --- [      Thread-11] c.n.d.guide.application.MyApplication    : Sleep 5秒之后,【子】线程ThreadLocal:{4=MyContext}  2020-10-18 18:38:27.776 INFO 3780 --- [      Thread-12] c.n.d.guide.application.MyApplication    : Sleep 5秒之后,【子】线程ThreadLocal:{5=MyContext}  2020-10-18 18:38:27.780  INFO 3780 --- [      Thread-13] c.n.d.guide.application.MyApplication    : Sleep 5秒之后,【子】线程ThreadLocal:{6=MyContext}  2020-10-18 18:38:27.783 INFO 3780 --- [      Thread-14] c.n.d.guide.application.MyApplication    : Sleep 5秒之后,【子】线程ThreadLocal:{7=MyContext}  2020-10-18 18:38:27.787 INFO 3780 --- [      Thread-15] c.n.d.guide.application.MyApplication    : Sleep 5秒之后,【子】线程ThreadLocal:{8=MyContext}  2020-10-18 18:38:27.789 INFO 3780 --- [      Thread-16] c.n.d.guide.application.MyApplication    : Sleep 5秒之后,【子】线程ThreadLocal:{9=MyContext}  2020-10-18 18:38:27.792 INFO 3780 --- [      Thread-17] c.n.d.guide.application.MyApplication    : Sleep 5秒之后,【子】线程ThreadLocal:{10=MyContext}

完整示例,请参考https://github.com/Nepxion/DiscoveryAgentGuide。上述自定义插件的方式,即可解决使用者在线程切换时丢失ThreadLocal上下文的问题

附录

Nepxion Discovery Agent Discovery【探索】微服务企业级解决方案

① Discovery【探索】微服务企业级解决方案文档

  • Discovery【探索】微服务企业级解决方案(PPT版) : http://nepxion.gitee.io/docs/link-doc/discovery-ppt.html

  • Discovery【探索】微服务企业级解决方案(PDF版) : http://nepxion.gitee.io/docs/link-doc/discovery-pdf.html

  • Discovery【探索】微服务企业级解决方案(HTML版) : http://nepxion.gitee.io/docs/link-doc/discovery-html.html

② Discovery【探索】微服务企业级解决方案源码。请访问Gitee镜像获得最佳体验

  • 源码Gitee同步镜像 : https://gitee.com/Nepxion/Discovery

  • 源码Github原镜像 : https://github.com/Nepxion/Discovery

③ Discovery【探索】微服务企业级解决方案指南示例源码。请访问Gitee镜像获得最佳体验

  • 指南Gitee同步镜像 : https://gitee.com/Nepxion/DiscoveryGuide

  • 指南Github原镜像 : https://github.com/Nepxion/DiscoveryGuide

④ Discovery【探索】微服务框架指南示例说明

  • 对于入门级玩家,参考 指南示例极简版 : https://github.com/Nepxion/DiscoveryGuide/tree/simple,分支为simple。涉及到指南篇里的灰度路由和发布的基本功能, Nepxion Discovery Agent 参考 新手快速入门 : https://gitee.com/nepxion/DiscoveryGuide/blob/simple/GUIDE.md

  • 对于熟练级玩家,参考 指南示例精进版 : https://github.com/Nepxion/DiscoveryGuide/tree/master,分支为master。除上述《极简版》功能外,涉及到指南篇里的绝大多数高级功能

  • 对于骨灰级玩家,参考 指南示例高级版 : https://github.com/Nepxion/DiscoveryGuide/tree/premium,分支为premium。除上述《精进版》功能外,涉及到指南篇里的ActiveMQ、MongoDB、RabbitMQ、Redis、RocketMQ、MySQL等高级调用链和灰度调用链的整合

Nepxion Discovery Agent Polaris【北极星】企业级云原生微服务框架

① Polaris【北极星】企业级云原生微服务框架文档

  • Polaris【北极星】企业级云原生微服务框架(PDF版) : http://nepxion.gitee.io/docs/link-doc/polaris-pdf.html

  • Polaris【北极星】企业级云原生微服务框架(HTML版) : http://nepxion.gitee.io/docs/link-doc/polaris-html.html

② Polaris【北极星】企业级云原生微服务框架源码。请访问Gitee镜像获得最佳体验

  • 源码Gitee同步镜像 : https://gitee.com/polaris-paas/polaris-sdk

  • 源码Github原镜像 : https://github.com/polaris-paas/polaris-sdk

③ Polaris【北极星】企业级云原生微服务框架指南示例源码。请访问Gitee镜像获得最佳体验

  • 指南Gitee同步镜像 : https://gitee.com/polaris-paas/polaris-guide

  • 指南Github原镜像 : https://github.com/polaris-paas/polaris-guide

请联系我

微信、公众号和文档

Nepxion Discovery Agent
Nepxion Discovery Agent
Nepxion Discovery Agent

本文分享自微信公众号 - Nepxion开源社区(iMicroService)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Opentracing + Uber Jaeger 全链路灰度调用链,Nepxion Discovery
当网关和服务在实施全链路分布式灰度发布和路由时候,我们需要一款追踪系统来监控网关和服务走的是哪个灰度组,哪个灰度版本,哪个灰度区域,甚至监控从HttpHeader头部全程传递的灰度规则和路由策略。这个功能意义在于:不仅可以监控全链路中基本的调用信息,也可以监控额外的灰度信息,有助于我们判断灰度发布和路由是否执行准确,一旦有问题,也可以快速定位
Stella981 Stella981
3年前
Nepxion Discovery 5.5.0 发布
!(https://oscimg.oschina.net/oscnet/f81c043194ef4732880459d00c1a720e.png)发布日志功能更新:增加基于Opentracing调用链的支持,目前支持UberJaeger,实现在SpringCloudGateway、Zuul和服务上的灰度
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这