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端的交互,以及如何实现一个简单的参数拦截记录功能。从整个实现机制层面描述其工作原理。

(更多…)

使用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());

(更多…)

使用guava Striped中的lock导致线程死锁的问题分析

2016/11/19 15:04:00 No Comments

在最近的开发调试当中,一个多线程的项目出现了线程死锁的问题。按照正常的解决思路,上监控工具yourkit或者自己打印出线程信息,最终的显示信息均为lock对象先拿到了自己的锁,然后去拿对方的锁,现状是这样的。但与一些正常的项目相比,按照正常的执行情况,是不可能出现死锁的。

项目使用了程序锁来保证并发情况下,一些操作只能由单个来执行,如一些不允许多次运行的场景。所谓程序锁,即通过由锁池获取相应的lock对象,然后进行lock操作,在相应的方法体内进行相应的控制。
项目中使用基于guava的striped来获取不同的锁,本来原来的代码是自己封装实现,但考虑到guava已经提供,因此直接使用了,相应的伪代码如下所示:

        Striped<Lock> lockStriped = Striped.lazyWeakLock(1024);

        Lock lock = lockStriped.get("动态生成的临界资源标识符");
        lock.lock();
        try{
            //执行业务操作
        } finally {
            lock.unlock();
        }

以上的代码因为足够的通用,因此被封装为一个LockAdvice,以实现通用的锁方法操作,只需要在需要加锁的方法上加上Lockable注解,就可以达到所要的效果。出现死锁的2个线程表现如下:

//线程1
加锁获取资源类型1
加锁获取资源类型2

//线程2
加锁获取资源类型1
加锁获取类型类型2

从上面的线程操作来看,加锁的顺序是一样的,释放锁的顺序也一致,因此理论上不会出现死锁。为保证锁切面的简单实现,上面的锁资源类型1 被表现为一个字符串,即通过不同的字符串来进行描述。也就是说,在上面的现象中,会有4个字符串进行锁的key进行操作。

问题场景被发现了,并且这个问题肯定会产生,多线程运行一段时间之后就会发生。查找问题产生的原因却很漫长。不知道为什么,使用jstack却没有打印出每个线程在哪一个环节持有哪个锁(是不是本身就没有),通过jstack的信息表示,相应的线程肯定持有对方的锁,而对方也在等级已方的锁,并且相应的锁的发生点也确定是在相应的锁切面准备执行的时候。剩下的问题就是看是否是相应的锁生成器striped本身就返回了相同的锁,如果由锁生成器本身就返回相同的锁,那么就肯定有问题了。

(更多…)