ZooKeeper Watcher 机制源码解释(转)

2018/03/13 10:09:18 No Comments

本文转自 https://www.ibm.com/developerworks/cn/opensource/os-cn-apache-zookeeper-watcher/index.html

ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。

ZooKeeper 的 Watcher 机制主要包括客户端线程、客户端 WatchManager 和 ZooKeeper 服务器三部分。在具体工作流程上,简单地讲,客户端在向 ZooKeeper 服务器注册 Watcher 的同时,会将 Watcher 对象存储在客户端的 WatchManager 中。当 ZooKeeper 服务器端触发 Watcher 事件后,会向客户端发送通知,客户端线程从 WatchManager 中取出对应的 Watcher 对象来执行回调逻辑。

    private final HashMap<String, HashSet<Watcher>> watchTable =new HashMap<String, HashSet<Watcher>>();

    public synchronized void addWatch(String path, Watcher watcher) {
        HashSet<Watcher> list = watchTable.get(path);
        if(list == null) {
            list = new HashSet<Watcher>(4);
            watchTable.put(path, list);
        }
        list.add(watcher);

        HashSet<String> paths = watch2Paths.get(watcher);
        if(paths == null) {
            // cnxns typically have many watches, so use default cap here
            paths = new HashSet<String>();
            watch2Paths.put(watcher, paths);
        }
        paths.add(path);
    }

(更多…)

使用greys找到泄漏的本地线程变量值

2018/03/12 12:17:54 No Comments

在程序代码中,出现过这样一种情况,在一个新的线程组中,尝试去获取一个threadLocal的值,按照相应的程序代码,应该是不能获取到的。但是在实际的运行过程中,却发现能够获取到相应的变量值。
在我们的程序代码中,使用了shiro来存储相应的用户信息,即一个简化版的的权限管理程序。其中,在标准的web程序中,shiro拦截器会把相应的会话信息存储在session中,并且在应用代码中,可以通过一个threadLocal的ThreadContext来获取相应的subject,进而拿到相应的会话值.相应的代码如下所示:

    public static UserId getUserId() {
        Optional<Subject> optional = Optional.ofNullable(ThreadContext.getSubject());
        return optional.map(t -> t.getSession(false))
                .map(t -> t.getAttribute("userId"))
                .orElse(null);
    }

因为相应的数据为web调用时才会注入到sessionId,而如果是一个定时类的任务,那么理论上应该是不会有相应的session对象存在,那么即不会有相应的subject存在,在调用此方法时即会返回在调用getSubject时失败。但是在实际上时,却发生调用subject不为null,进而在调用getSession方法时出现了错误信息。

相应的调用链看起来应该像是这样

  1. 线程1调用了 ThreadContext.setSubject 方法,设置了session信息
  2. 线程1完成整个业务方法的运行,相应的session信息被销毁,但相应的removeSubject方法并没有被调用
  3. 线程1重新以任务的形式执行任务代码,其中调用了getSubject,但不能拿到相应的数据

在这种调用链中,我们只看到了最终的现场,但原始的设置值的现场即不能复现,即不清楚数据是什么时候,哪个调用任务进行处理的。整个问题称之为数据泄漏,即数据在不应该出现的地方出现了。
处理这种问题,一种作法即是异常标记法。详细的步骤如下所示:

  1. 在调用 ThreadContext.setSubject 时,设置一个异常线程栈(Exception),此里面封装了当前调用的整个路径
  2. 将异常线程栈(字符串形式)存储在一个threadLocal变量中,方便后面获取
  3. 在调用 getSubject 时,如果发生了相应的错误信息,即表示复现了相应的错误场景
  4. 这时候即将之前存储到threadLocal中的字符串获取出来,打印出原始的setSubject调用路径

整个作法参考于alibaba druid的数据库连接泄漏检测。

本文介绍了在不修改源代码的情况下,并且在线上环境,使用greys,来监控相应的调用,当出现了满足条件的场景时,自动打印出相应的调用路径。

(更多…)

基于jgroups分布式缓存实现redis服务的fallback处理

2018/02/06 21:36:57 No Comments

现在的系统当中,实现缓存均是采用redis来完成,一个典型的场景即是将用户的会话信息存放于redis当中,以实现分布式缓存以及服务的高可用。当nginx在进行请求分发时,不需要采用ip hash,随机分发请求,后端直接读取中央缓存即可完成相应的业务处理。但这种场景同样存在一个问题,即是缓存的高可用问题。特别是相应的redis服务由外部提供时,这个问题更加明显。初期redis服务为一个单点时,当服务挂掉时,会导致整个服务将不再可用。

解决此问题有2个方案,一个是再使用同样的策略保证redis的高可用,如部署redis集群或者是sentinel模式,另外一种是直接放弃对redis的高依赖性,在应用层直接进行缓存的fallback支持。在本场景中,我们仅将redis作为一个缓存层使用,它的一些其它特性如发布订阅暂不考虑。

在本文中,我们使用了hystrix+caffeine+jgroups的方案,来完成整个redis的fallback化处理,通过hystrix实现对redis调用的断路及fallback处理,使用caffine提供高效本地缓存,使用jgroups完成多系统间的数据复制分发。 主要实现的目的在于,当redis挂掉之后,能够切换到本地分布式缓存,保证系统后续可用(不过之前存放在redis中的数据完全不再可见,会导致一部分的问题,如会话信息会丢失。可考虑仅将会话信息直接复制在本地缓存当中)

本方案适用于redis短时间由于网络或其它原因连接不上,但在相对短的时间内(如几分钟,不超过1个小时等)即会恢复的情况,这样本地缓存不会存储很多东西的情况.

(更多…)

greys在线诊断工具的主要实现

2018/01/17 17:39:44 No Comments

greys是一个使用java management tool进程注入javaagent实现在线系统的诊断一个工具。原github为(https://github.com/oldmanpushcart/greys-anatomy),其主要的功能在于系统不停机的情况下。可以查看系统中的线程信息,cpu使用情况,jmx信息,以及某个方法在运行时的调用栈,调用参数等。

一个典型的场景就是线上某个功能出bug,但是系统中并没有记录参数信息,这时候即可通过这个功能注入agent,临时地打印出这个调用方法的参数,以方便定位相应的问题。如无此工具,则只能改代码,然后重新上线。在这种情况下,可能出错的场景就不能再复现。(当然也有其它工具(如log monitor)作到在线系统参数记录开关的目的,这里不作描述)

本文主要描述greys是如何工作的,包括如何注入到在线系统,然后脚本client与注入后server端的交互,以及如何实现一个简单的参数拦截记录功能。从整个实现机制层面描述其工作原理。

(更多…)

使用javassist编写一个简单的agentClassTransformer

2017/11/09 15:56:32 No Comments

前段时间找到一个很好的工具Greys-anatomy, 相应的参考地址为参考地址:https://github.com/oldmanpushcart/greys-anatomy. 为此,专门研究了一下基工作原理.并简单研究了一下基于Instrumentation进行运行时代码调整的一些实现手法. 本文, 简单介绍一下如何使用javassist来简单对一个代码作编织, 实现简单的一些监控指标手段.

附: 另外,前几年淘宝也简单作了一个通过启动时agent达到运行方法监控的目的, 称之为TProfiler,对研究一些实现手法很有用.

一个主要的ClassFileTransformer定义如下:

    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

传递到当前实现的主要有用的信息即为className, classfileBuffer两个数据. className即为当前正准备加载的类, 而classfileBuffer即传递给当前处理的字节码, 需要做的即是将这个字节码进行处理,然后返回一个已经处理过的字节码,即完成instrument操作.

(更多…)

使用spring boot和embedded tomcat开发时出现404找不到jsp问题的处理

2017/06/06 20:16:17 No Comments

初使用spring boot时,出现程序中配置了相应的指向地址,但前端界面始终报找不到jsp界面的404错误.但相同的程序在另一位同事时却正常工作.因此,有必要从源码角度分析一下,spring boot如何和embedded tomcat进行相应的整合.

本文开发时使用的为idea,相应的jsp放置在标准的src/main/webapp目录下,并且spring mvc也配置了 spring.mvc.view.prefix各项网上都能找到的配置信息.

首先是先将一个最简单的jsp文件放置在src/main/webapp,然后启动程序,直接访问此jsp(不通过mvc跳转), 仍然返回404.这就表示实际上这个src/main/webapp并没有被spring boot中tomcat所识别. 一般认为这个目录就是一个放置资源信息的地址目录,那么这里404表示在启动过程中,这个目录并没有被正常找到.

因为spring boot并没有一个显示配置项来配置此位置(从hello的demo来看),因此此地址的查找为一个简单的逻辑过程.从tomcat的角度来看,这属于一个documentRoot,即项目文件根目录,从尝试从整个源码中搜索有关于documentRoot,或者说对于tomcat一个上下文来讲为docBase的概念.最终找到的位置为 TomcatEmbeddedServletContainerFactory#prepareContext 方法.

简单的代码来看,主要的代码如下所示:

		File docBase = getValidDocumentRoot();
		docBase = (docBase != null ? docBase : createTempDir("tomcat-docbase"));
		context.setDocBase(docBase.getAbsolutePath());

(更多…)