参加职业规划分享的小感想

昨晚参加了雄哥的职业规划分享,很朴实,甚至如某同事说的有点木讷,但总体来说我还是有一些收获的。

其实我个人是有点诧异的,就是说假设我再过几年是怎样的一个情况,我自己能不能有没有这样的心态,跟伙伴们分享这些经历的事情和教训,分享自己的感受。有没有这个勇气,我很佩服的就是这个勇气。是的,可能会感觉到中年危机,还能不能有好的心态?

过程中提到个人总结的一些对事情,对工作的看法,例如他提到怎么去保持一个良好的心态,要专注去做一些事情,要一步一步的去做。坦白的说,我印象并不深刻,我记忆力比较差,提问的时候我都忘记first part是说什么了。但我感受比较深的还是怎么去做一些事情。是的,有flag很重要,到远远不够。让你今天看10页书,再忙加加班还是可以搞定的,但每天看1页书却并不容易。有flag了,或许分解过后感觉也并不难,但实际做下来就这么难,就这么神奇。

我们说这是自律,执行力,刻意练习….各种各样成功学,方法论都会遇到的问题。人的惰性是非常强,甚至我认为人骨子里就是这样。不过也有一些小技巧,例如雄哥他老婆日复一日的督促,我们也可以立公开的flag,但又不是困难,飘渺的,甚至我就说今天要读10页书,在群里或者在你的桌面上,首先说一句比真正去做容易,你很容易完成,另外大家都看到了,会起到督促的作用。

还有一些关于技术学习方向上的问题,例如打好基础,框架学习,业务领域学习之类,我对这些也比较同意,其实工作久了,感觉学习能力反而更强了,效率并不差,某同事说这叫飞轮效应,有兴趣的同学可以自行了解。我主要强调的是,对于大多数行业新人来说,前面3-5年打好基础真的非常重要。

当然某些方面,我也有一些不同意见,例如推荐道德经,我不是反对道德经(其实我也没看过,或许以后有些兴趣),我只是不喜欢强烈荐书,只有大家愿意去尝试做这个事(读书),才能体会’纸上得来终觉浅’是不是真的浅?

最后,感谢分享。

redis故障后记

最近线上系统还是不太平静,周五redis宕机又来了,暴露了不少问题。

系统中的代码是模仿jedis的源码实现,用jedis再封装的一套哨兵+分片的实现。回去把哨兵的机制又仔细学习了一遍,通过分析代码实现机制,初步分析有可能是客户端在处理主从切换上有些问题。

客户端库相关的issue

  • https://github.com/redis/jedis/pull/1566 bug描述:主从切换时,如果客户端和redis断开,会接收不到主从切换消息,导致保留旧的redis主信息
  • https://github.com/redis/jedis/issues/1910https://github.com/redis/jedis/pull/1911 bug描述:initPool和哨兵监听线程并行执行时存在race condition,可能无法感知到internalPool已经变化,导致保留旧的redis主信息

我们平时搞了那么多质量活动,根因分析,版本回溯,什么规范,流程, Checklist等,一个比一个厉害。结果还是该来,终究还是会来,陷入一个无限循环的怪圈中。

不过我们还是要吸取一下教训,看看后面有什么可以改进的, 简单写三点。

  • 核心中间件的掌控力。其实我们已经出现过多次的中间件相关的问题,通常我们都缺乏定位手段,甚至定位方向也比较抓瞎。主要表现在对工作机制原理理解不足,对中间件的配置选项不熟悉,对常见问题没有摸底预案,无法从报错现象,日志信息中快速定位问题。如果是开源中间件的话,其实资料案例还是比较多的。爹有娘有,不如自己有。这一块计划对目前使用的核心中间件进行梳理,按责任田的方式要求主动深入学习细节,总结分享。
  • 底层代码,公共代码要谨慎。上次有人调用redis的时候也出过问题。我们现在有些关键代码,底层代码都是由普通开发人员完成的,在可靠性,维护性,性能方面可能都存在隐患。最近的多个问题,也让我们感觉到,即使有经验,也要小心翼翼才可能写好。目前对这个方式主要还是committer要意识到风险,预防为主,让经验丰富的人去修改,找经验丰富的人来评审。
  • 可靠性测试要重视开展。这次的问题也暴露了我们怼非功能测试投入不足,对例如可靠性的场景测试的长期忽略。不过最近我们测试的同事也是意识到这一块的重要性的,例如尝试引入混沌工程的做法。特别是后续这种分布式,微服务构建的应用系统普遍存在,关系错综复杂,故障点可能出乎意料,上线有可能会出现各种奇怪的现象,甚至连锁反应,对系统的稳定性是极大的挑战。

Java类静态代码块并发案例

我们某个服务,请求报文会有一个操作码的一段,代码中会根据操作码,找到对应的执行类,然后执行对应的接口方法。而大多数的执行类实现是会根据操作码,再分发到不同的方法去的。

但是生产系统偶尔出现一些找不到处理方法的情况,本地测试又没有遇到过。而且,有时候服务重启一下,就正常了。

问题分析

我们排查了所有的操作码,没发现有漏配置的情况,也就是说,方法是存在的,只是找不到。

再分析,有可能的就是哪里有并发的问题。

这里再说一下我们的数据结构和处理流程是怎样的。

这个代码是从一个大规模c++业务系统转过来的,通常在java里边是配置文件来处理这些处理方法的对应关系的,为了保持代码不大调整,就用了一些取巧的方式。

  • 在基类上定义了一个protected静态的hashmap
  • 在多个子类上用静态代码块给这个map进行方法注册
  • 在应用启动的时候,扫描所有的类,把类名和操作码关联上(因为配置上还要兼容原c++代码,所以用的是simpleName,不能直接反射)
  • 在请求过来后,通过操作码拿到执行类,反射new一个实例,这样就触发静态块的执行,达到注册的目的
  • 可能很多人也注意到了,这里用到hashmap会不会有问题。按以前的习惯,一般静态块主要是初始化数据,在真正时候的时候,都是固定的值,用hashmap是可以的。

不过,这里是有些特殊的。我们知道一个类的静态块是可以保证线程安全的,只会执行一次,但是多个类的静态块是不是顺序执行的,这个以前是没有特别注意的。找了一些java语言规范,也没有提到这个内容。既然没规定,隐隐觉得这可能还真是问题。

写个例子测试一下。

class A {
    static {
        System.out.println("A =>" + new Date());
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("A =>" + new Date());
    }
}

class B extends A {
    static {
        System.out.println("B =>" + new Date());
        try {
            Thread.sleep(10000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("B =>" + new Date());
    }
}

class C extends A {
    static {
        System.out.println("C =>" + new Date());
        try {
            Thread.sleep(10000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("C =>" + new Date());
    }
}

public class Test {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                new B();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                new C();
            }
        }).start();
    }
}

结果如下,的确是可以并行执行的。

A =>Wed Jan 06 10:42:33 CST 2021
A =>Wed Jan 06 10:42:34 CST 2021
C =>Wed Jan 06 10:42:34 CST 2021
B =>Wed Jan 06 10:42:34 CST 2021
C =>Wed Jan 06 10:42:44 CST 2021
B =>Wed Jan 06 10:42:44 CST 2021

这样就可以解释这个现象了。我们知道hashmap在并发情况下有可能会死循环cpu100%,还有一个可能就是并发put可能丢数据。以1.6的hashmap实现为例子(相对1.8比较简单)。addEntry(hash, key, value, i),如果有产生哈希碰撞, 导致两个线程得到同样的bucketIndex去存储,就可能会出现覆盖丢失的情况:

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

解决办法也很简单,改成线程安全的concurrenthashmap.

经验总结

  • 多个类的静态代码块是可以并行执行的,不要在多个类的静态代码块中共享数据
  • 确实要共享数据,要注意线程安全问题,考虑使用线程安全的对象

一次问题求助的对话记录

下午的时候收到一个消息,内容如下(ip采用字符代替):

XX我想请教一个问题

短信网关我模拟了生产那种nfs同步的形式我在 xx.xx.xx.xx以及yy.yy.yy.yy上都部署了短信网关进程 XService,然后启动实时接口逻辑,在zz.zz上部署了一个处理进程,已经共享了一个内存给上述2机正常逻辑应该了通过日志生成个文件然后同步到zz.zz上处理移到别的地方,但是现在的情况是日志被处理移走后,在另一个目录继续收来自xx.xx个yy.yy的日志数据然后rollover再在原目录生成一个新的文件再移到新目录上继续记录我想问问这个情况是什么地方有问题导致的

整整一段话,我看得有点懵逼,我回复说,看不大懂,逻辑有点乱。他说过来说,我刚想说先整理整理,就看到他人已经过来了。

于是我就说看不懂,先整理整理吧,我把那段话拷贝到文本编辑器里边,边问边改。

下面是主要对话:

我:说了怎么多,想说什么问题?
他:有两个机写文件…然后同步到另外一个机器,这个程序处理后移走后还继续有写…
我:用nfs挂载目录,一边在写,另外一边把它移走,但还会继续写这个文件?
他:嗯
我:好几个ip写来写去,可以用A,B,C代替一下,在前面标注一下。
我:那么操作步骤是这样的?在A,B,C机器都挂载了nfs目录,在A,B部署了短信xx进程,在C部署了xx处理进程?(边说边写),两个进程什么关系?
他:A,B机器的程序会写日志文件,C机器的程序会处理日志文件并移动到另外一个目录。
我:这不大规范呀,移动文件得保证没人写了才是正常做法。
他:我是猜测一下生产的某个报错是不是这个原因,生产是会报错,而这里还会继续写。
我:嗯,那预期情况是正常不再写?实际情况是移动到另外目录后还有数据写入,并且发生轮转后是在原目录生成新文件。
….我想了想
我:我对nfs不怎么熟悉,感觉有写入的情况移动文件,报错或者继续写入都是可预期的情况,这个得找找资料。不过轮转后文件在原目录出现应该没错的。对> 了,你是怎么写日志的。
他:我给你发了配置,是用log4j写日志的方式。(我瞄了一下,的确是)生产是6个机器写入文件的。
我:多个机器写同一个文件,这也不大规范吧?
他:文件是有区分区域的,正常只有一个写
我:那就没问题,不过你就不需要说A,B两个机了,说一个就可以了。
他:生产是会报错,但这里会继续写入。
我:nfs的行为还要要再看看,但是移动文件应该确保已经没有写入了。
他:这次就是改造成轮转之后再移动文件。
我:先这么处理,问题这还要再看看。

他就回去了,我把整理后的内容发给他,希望他下次要找求助,可以把情况描述清楚一点。

晚上的时候,突然想到,整理后的内容格式,问题,步骤,期望结果,实际结果,这不就是我们日常的报告bug的常见模板么?

世界真奇妙。

关于日志打印行号的性能案例

问题描述

上个版本快上线的时候,发现系统整体变慢了。观察页面请求耗时,存在不同程度的性能倒退。观察日志,也没有发现有明显的异常。

分析过程

页面请求后面还有一堆的接口服务,首先需要定界,还好有服务调用链可以观察。挑选了一个耗时变化比较大的请求,查看调用链信息。发现基本都消耗在接口页面请求的服务上了。

服务调用链

了解具体应用,定位就相对简单了。定位到是哪个系统后,本地同时发起多次请求,然后打一下堆栈,就可以看到基本都挂在日志输出上面。可以看到堆栈上有获取获取堆栈信息的方法调用。

at java/lang/J9VMInternals.getStackTrace(Native Method) 
at java/lang/Throwable.getInternalStackTrace(Throwable.java:268(Compiled Code)) 
at java/lang/Throwable.printStackTrace(Throwable.java:520(Compiled Code)) 
at java/lang/Throwable.printStackTrace(Throwable.java:301(Compiled Code)) 
at …/debug/LocationInfo. (LocationInfo.java:84(Compiled Code)) 
at …/DebugLogImpl.doLogPre(DebugLogImpl.java:492(Compiled Code)) 

看到获取堆栈信息,我就有大概知道是什么回事了。一开始我看了一下对应的log配置,并没有发现有添加打印行号的日志格式,回头去看代码才定位到最初的地方。

原因梳理

其实这个代码很早就有的了,碰巧是其他几个地方改动把这个问题暴露出来了。有三个点:

  • 原本系统在打印日志是warn级别的,这次版本加入调用链改造把日志级别调整成debug,方便后续做动态调整。
  • 系统用的是另外一套系统带过来的一个日志实现,代码里边默认就会获取行号(调用堆栈的地方),而不是通过log格式配置实现的。
  • 系统里边有一段对象转换的逻辑,递归调用的并且调用非常频繁,在代码里边有写调试日志,日志级别用的debug。

几个点结合到一起,问题就暴露出来了。单单看一个问题并不复杂,很多时候问题的原因也是非常简单的,但是在复杂的系统调用中,就没那么清晰了,需要借助一些监控分析工具。

知识复盘

据我了解,在应用代码中主动new Exception的做法,除了用来控制业务流程之外(不推荐),还有两个应用场景,主要用在一些框架上:

  • 一种是为了记录当前堆栈,主要用来对象池技术上用来调测对象泄露。
  • 一种是为了拿到调用行的行号,主要用在日志框架上。 这里碰到的就是第二种应用场景,虽然一些开源日志框架带有这个功能,一般是要谨慎打开的。至于对象池上的应用,例如数据库连接池,一般也是建议有泄露的情况才打开这个特性。

我们知道try-catch并没有多大影响,但new Exception是有比较大消耗的,因为会记录调用堆栈,需要避免频繁调用。

也有一种技巧,是不让异常记录堆栈,这种做法要区分是哪里的问题,可能就得添加一些附加信息,例如自定义错误码。

public class EmptyStackTrace extends XXException {
    @Override
    public synchronized Throwable fillInStackTrace() {
        return this;
    }
}