浅谈如何做好一名技术面试官

前两天有人找我周一给大家做个做个技术面试的赋能培训,我上公司内网找一下有没有合适的赋能材料,想一下偷懒,结果没找到,最后还是得按自己的思路来,顺便借这个机会总结一下以往的经验教训。

结合当前团队人员招聘的现状,我主要讲几个方面的事情:

  • 面试官的原则和心态
  • 岗位要求的解读
  • 技术面试的参考做法

面试官的原则和心态

首先,面试选人是非常重要的工作(如果不认同,请绕道)。作为技术团队来说,技术面试尤为重要。但目前面试过程还是比较随意的。作为面试官,希望做到:

  • 相信自己的判断,甚至这只是第一感。本来面试时间就不长,能力可能没挖掘出来,问题可能也没暴露,不苛求但要做出自己的判断。
  • 不要受招聘进度过度干扰,最终毫无底线。项目组有时候会要人要得很急,但我们不是靠堆人来做工作,要在短期掌握业务也没那么容易,加人的短期效果并不明显。另外,来面试时间也要有一定规定,约定人数,特定时间段,避免过多干扰工作。
  • 谨慎招聘低于平均水平的。什么是平均水平,主要看你周围的同事。这样才有可能让团队向上走。如果遇到水平实在太差,严重造假等情况,也要反馈,要求前头把一下关。
  • 你选人,人选你,面试你代表门面,平时要充实自己,不要被人笑话。现在的岗位分类还是比较细的, 面试官也要专业对口的,例如不要java的去面c++岗位的。
  • 不要一来就非常奇怪或偏门的问题,要循序渐进。

岗位要求的解读

我们的目标是寻找人员能力经验和岗位要求匹配度比较高的面试人员。但岗位要求,说真的,市场经济,有多少钱办多少事。但无论怎样,在团队中如果有太多小白,协作成本非常高,加人只是负作用。

所以,对岗位的要求,实际面试中,也不是完全参考的。要看面试人员的经验和岗位要求的匹配度,也要看面试人员的掌握程度,相关技能的适配程度。岗位要求分清楚有哪些是必须的,不然培养成本太高,有哪些是可选的,有就挺好,没有关系不大。人员能力中哪部分是强项,哪部分是弱项,即使和岗位要求有些差异,但是不是学习起来并不会太难?

例如我们招聘大多数技术岗位,都是要求有java的编码经验,很可能是必选的,因为我们说java通常会附带相关的框架经验,那么只有python经验可能转过来成本比较高,就不合适了。

但如果要求mybatis框架经验,面试人员没用过,但面试人员用过其他的,例如hibernete,这可能就也可以接受,但你要了解mybatis和hibernate的差异,知道关键点在哪,根据关键内容的掌握程度来判断新框架的学习成本。

技术面试的参考做法

N连问的考察方式

这种方式在网上也比较常见,就是针对同一个知识点,多提几个问题来考察应聘者对知识和技能的掌握水平

比如对方的简历上写着熟悉多线程,你就可以这样来提问。

1)你了解多线程吗?

对方可能会回答:“了解”、“熟悉”、“用过”。你可以接着问。

2)你们多线程用在什么场景,用到库之类?

对方可能巴拉巴拉说一堆。对方可能会提到线程池,你接着问。

3)JDK自带的线程池机制是怎样的?

那么还可以接着问。

4)JDK自带的线程池默认带了哪些实现?

对方可能能举出有哪些实现,每个实现有哪些缺点之类。那你还可以继续。

5)怎么评估线程池大小,能不能动态调整池的大小?

这个可能就比较麻烦了,对方可能能答上一些,那你还可以在操作系统机制,实现方案上继续讨论。

项目经验描述+引导

不得不说,很多面试人员,简历写的很不好,再加上自己介绍也比较简单的。

我们可以先让面试人员介绍一下自己熟悉的项目,有什么重要的业务流程。这个业务流程采用什么技术实现的。

对方可能会说一下业务流程是怎样的。(我们主要关心有什么比较典型的业务场景,和技术方案),用的什么框架,用了什么中间件,大体是怎样的巴拉巴拉。

你可以问问为什么这么做,有没有其他方案。

也可以问问各个组件是怎么通信的,数据结构是怎样的,巴拉巴拉..

当然,你关心或感兴趣的地方,可以用N连问的方式挖掘一下。这个过程主要是感受一下面试人员的分析、理解的能力如何,沟通能力如何,能不能把事情讲清楚

“走马观花”找亮点

这个主要是面试之前做的,拿到简历之后,浏览简历看看待会问什么问题,同时看看有什么有趣的、不同的内容,提前准备

我一般会关注一下:

  • 工作经验怎样,工作经验长的会根据情况提高期望,工作经验少的多关注基础、学习能力。
  • 知识面怎样,到时候可以多关注会什么,而不是不会什么。例如一般前端,那么会点后端就是加分项,当然主要关注点还是前端能力。
  • 有没有总结分享的习惯,例如有没有博客、开源项目之类,有时间的话我会上去看看。
  • 有没有学习的习惯,例如有时候会问问最近学什么,看什么书,当然也要求讲讲具体内容是什么。
  • 学历怎样,行业经验怎样,有什么竞赛经验之类(或多或少会有第一印象,但我们要尽量避免定型)。

白板面试

主要针对开发人员的,也经常使用,都是一些算法题。虽然很多人抗拒这个,但在难度适中的情况下,我还是比较乐意推荐的。一般的初中级开发人员,我选择的问题:

  • 不用一些特别的算法,例如动态规划之类
  • 代码长度不长,在20行左右的核心逻辑
  • 避免技巧性过高、过于数学化的情况
  • 需要顺便回答一点算法时间/空间复杂度判断或优化

总结

从团队的角度,每个公司每个团队都有自己的风格,所以招聘风格也是有所差异的,有些精益求精,有些大包大揽,有些深挖底层,有些硬钢算法。

从个人的角度,我个人虽然参加过很多次面试,但回想下做得也并不是很好,能趁此机会总结一下也好。我发现,很多时候前面一两分钟的感觉很大程度就决定后面要不要,基本上问问题也有一部分是在照顾对方情绪而已。

所以很多东西无所谓好坏,形成自己的风格和判断最重要。

对检查列表的一点看法

什么是检查列表(checklist)?

我们一个项目告一段落之后,通常会有一些知识沉淀。这些沉淀的意义在于同类型项目具备哪些指导意义,在后续做同类型工作的时候,提前发现一些问题。

其中有一个常见的措施就是检查列表(checklist),对着列表一项项,不容易遗漏重要问题,可以及时排查隐患。

检查列表有什么标准?

在最近的xx化项目里面,检查列表同样也是很有必要的。首先它是一系列项目工程的改造,具备一定的共性,另外大家普遍对相关技术比较陌生,工作开展很容易只依赖某些人的经验。

我们检查了对应项目的检查列表,发现实际的指导意义不高。简单的说,就是看了也没什么用,不知怎么下手。

后来我们意识到这其实是一个普遍现象。

我们认为检查列表的核心是可操作性。这和规范的作用是有些差异的,规范是说我为什么要做这个,不做这个会有什么问题,什么才是正确的做法。但在实操过程中,我需要理解,这个东西我怎么去检查,检查标准,检查步骤是不是明确的。还有更进一步的是,检查列表也要适当关注检查效率,检查步骤是否工具化,还是说只能单靠人力。

让我们想想,假设我是一个新人,当我拿到检查列表的时候,对我的知识背景有什么要求,能不能帮助我开展工作,我按照这个检查列表去操作的时候,结果是不是可信的?

检查列表何去何从?

图例1

一般的情况是这样的。

我们会遇到各种各样的问题,在解决问题的过程中,逐渐发现某些问题存在共性,我们把这部分问题以规范、设计准则的方式固化下来,作为后续开展同类型工作的标准。但规范可能是比较详细的,方方面面都照顾了一下,在实际操作中不容易抓住重点,容易遗漏。我们再对规范中的要点进行提取,针对步骤、结果进行实例化,甚至进一步提炼检查工具,形成检查列表,希望可以更多更快的提前发现问题。

按照这个先后顺序,肯定是已经遇过问题,并且有意识的总结过。实际也是有的,只是存在某个人的脑子里边。如何形成知识管理,靠自觉是不大行的,主要靠督促,我的做法比较暴力,我看到某同学在处理某个问题,有思路解决之后,我会要求把输出成文档总结,对配合性比较好的同学,我还会对内容有更多要求。输出之后,再广而告之。

我们可以看到,一些特定领域的规范,已经可以直接跳过检查列表,完全的工具化。例如代码静态检查的findbugs、checkstyle之类,例如阿里的java规范,也可以通过pmd形成工具落地。例如安全相关的规范,也有一些检查工具。但大多数的场景,还是没有办法工具化的,或者部分工具化,我们更专注的是,如何才能更容易操作。

例如,我们发现更换环境可能存在网络放通问题。那检查列表可能就有一项检查网络放通,但要点肯定是知道有哪些地址需要放通。的确,这不直观,怎么提升可操作性呢? 可以先从单纯的思路开始,我们可以说,收集各个外围平台(从设计、测试、运维的角度来收集)。也可以说,根据代码中的相关代码来分析(例如网络访问的api等关键字),反推访问的地址。其中第二点是可能实例化的。虽然没法工具化,但这样是不是更好一点?

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

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

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

过程中提到个人总结的一些对事情,对工作的看法,例如他提到怎么去保持一个良好的心态,要专注去做一些事情,要一步一步的去做。坦白的说,我印象并不深刻,我记忆力比较差,提问的时候我都忘记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.

经验总结

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