版权所有,转载请注明原作者,仿冒侵权必究法律责任
1. Java 基础部分
1.1 谈谈单元测试和集成测试的定义和理解?如果一个方法中有远程调用,那么如何进行单元测试?如何使用单元测试测试远程调用异常的情况?
答:单元测试我的理解是后台开发人员在编写完某一个模块的功能代码之后,对每一个模块的功能代码进行测试,主要是使用一些给定的测试参数用来排查后台代码容易产生的各类异常问题,保证某一个功能模块后台代码的健壮性。单元测试的计量单位是模块和方法,严格来说指的是开发的每一个模块都需要进行单元测试,一般情况下根据项目架构的不同,对于模块划分的定义也是不同的。
比如在面向服务的SOA架构或者微服务架构项目中,我理解的模块是每一个服务项目中的每一个业务模块,具体的单元测试的每一个模块除了从项目架构的角度上来考虑划分的粒度之外,还要考虑到实际的业务场景,可以简单理解为对自己开发的业务模块的最细粒度的一种测试方式,根据业务复杂度可以考虑到针对模块中的每一个方法进行测试。单元测试是一种最细粒度的测试,是检查功能模块的代码健壮性的主要实现方式,是白盒测试最细粒度的一种实现,由开发人员直接在后台进行模块化测试,是保证开发代码每一个模块、每一个方法健壮性的最有效实现方案。优点是测试粒度很细,出现问题可以及时快速定位到问题,缺点是如果测试的粒度没有确定好,业务模块和方法数量多的情况下,效率很低。
集成测试我理解的是分为后台功能集中测试和前后台集成测试。后台功能集中测试对每一个服务的所有功能进行测试,集成测试一般会安排在单元测试之后进行,一方面集成测试是对单元测试结果的检验,可以暴露某一个单元测试的问题,一方面由于前后台分离开发,即使单元测试都能通过,也不能保证前后台请求响应、数据交互都是完全正常的,比如我经常遇到前后台参数界定不清晰,前台传递的参数后台没有准确接收,后台返回的响应数据前台没有正确渲染等等问题,那么为了解决这类问题,除了使用一些既定的方式比如Swagger API接口文档可视化、针对每次服务开发编写后台代码请求和响应接口文档之外,还需要使用集成测试来首先进行后台开发的所有模块的集成测试,一方面解决RPC远程调用服务通信异常、单元测试没有发现的问题,然后在进行前后台集成测试来确保模块正常进行前后台交互。
如果一个方法进行RPC远程调用的时候,单元测试需要定位到每一个远程调用的地方进行测试,如果是这种问题,我会把远程调用的方法先单独进行单元测试,如果远程调用的方法有问题,那么我会去联系提供这个接口实现的同事去检查他的接口实现。如果远程调用的方法单独测试都没有问题,那么我会在单独测试我方法里的代码,如果没有问题,再统一进行测试。
1.2 详细描述一个对象在 JVM 堆内的可能的生命周期?比如一个对象在哪块内存区域创建?YGC & FGC 之后的结果?越详细越好,考虑的情况越多越好。
答:
JVM堆采用分代算法来管理对象的生命周期。
对象在JVM堆中的生命周期分为新生代、老年代、永久代
新创建的对象默认优先分配到新生代Eden区,新生代主要有三块区域组成:Eden、Survivor S0(from)、Survivor S1(to)
新生代管理对象生命周期主要是使用复制算法实现的,每个对象每次执行复制算法会年龄+1,当达到默认分代年龄(默认15,可以通过JVM参数配置)之后,会把对象分配到老年代
第一次复制:Eden区满了之后,会进行一次YGC回收,把对象复制到S0区,
第二次复制:对象从Eden、S0区复制到S1区
第三次复制:对象从S1区复制到S0区
第4-15次复制:对象在Survivor幸存者区S0和S1之间来回复制,直到达到对象分代年龄后把对象分配到老年代
YGC是新生代的垃圾回收,每次执行YGC都会强制触发STW,暂停所有除了当前垃圾回收线程之外的其他线程执行
FGC=MinorGC+MajorGC,会对整个堆区间进行垃圾回收,如果频繁执行FGC会影响GC性能,需要考虑是不是出现了内存泄漏、内存溢出等问题。所以GC性能调优的最终目的就是尽量减少FGC执行册数
新生代对象分配到老年代主要有以下几种情况,可以理解为晋升:
常用的垃圾回收算法有:引用计数器、根搜索、标记清除算法、复制算法、标记压缩算法、分代算法
新生代回收主要使用复制算法实现,新生代对象活动比较频繁
老年代回收主要使用标记清除和标记压缩算法实现。
新生代常用的垃圾回收器是串行Serial、并行ParNew、吞吐优先Parralel-Scavenge
老年代常用的垃圾回收器是并发标记清除CMS
主流的HotSpotVM虚拟机默认采用ParNew新生代+CMS老年代实现垃圾回收
GC性能调优可以通过一系列JVM参数和GC命令实现,如jstack、jmap
1.3 以下程序会输出几次 Hello World. cnt = ? 为什么?
答:这个JDK1.8线程池的用法突然忘了,所以我不能乱猜结果,说下我对并发编程的理解:
并发编程这块我目前只对IO相关、线程的基本概念、线程锁、分布式锁、CAS无锁机制有一定了解,这块还在学习
说下对IO并发编程的理解:
BIO 默认的实现方式,同步阻塞操作,也就是如果IO操作响应时间长只能等待,线程阻塞
NIO 同步非阻塞IO操作,使用轮询的方式获取IO操作的响应结果,不会阻塞线程,但是高并发访问的时候,线程池开销大
AIO 异步IO操作,异步的方式实现IO操作,即使没有获取到IO操作执行结果,程序照样正常执行。
信号驱动IO:接收到给定的信号之后才会进行IO操作,不主动执行IO操作,必须先接收到对应的信号。
线程锁主要是一个JVM中多个线程并发执行时的线程安全问题,属于线程并发,可以使用悲观锁和乐观锁实现
分布式锁主要是多个JVM并行时JVM中的线程安全问题,属于进程并行,可以使用zookeeper临时节点实现,也可以用redis操作Key实现
CAS无锁机制实现是基于Unsafe类中的public static native final compareAndSwapInt方法实现,不为常用。
2. 中间件相关
2.1 列举你所知的常用的中间件框架、组件,并描述每个中间件使用场景。
答:列举几个常用的:
Redis分布式缓存:主要用来作分布式缓存和分布式数据共享,一般在保存Token令牌、短信验证码、缓存查询数据、访问计数器、实现分布式锁、延迟操作
Netty分布式网络通信:主要是基于NIO实现的一款支持多种网络传输协议的框架,本质是对JDK内置的NIO操作API的重新封装和完善,使得NIO操作更加易用,可以快速搭建服务器和客户端,
主要用来做基于TCP协议的网络通信数据传输。
Dubbo RPC远程调用框架:使用RPC远程服务调用实现服务之间的通信,底层实现是基于HTTP+Restful接口实现网络数据传输,Dubbo服务器端和客户端可以使用Netty实现,Dubbo提供了服务发现、服务治理、服务容错、负载均衡等策略,实现高性能RPC远程服务调用。Dubbo中的服务分为生产者、消费者,生产者发布服务信息到注册中心Zookeeper,消费中从Zookeeper中获取服务信息,典型的发布订阅模式实现。
Dubbo在传统SOA架构中使用的比较多,但是微服务项目一般使用Eureka或者Nacos作为注册中心,因为Dubbo配置比较繁琐、服务机制不够完善,不易于敏捷开发。
ActiveMQ消息队列:发布订阅模式实现数据同步,当需要实现数据实时同步的场景下,比如前后台管理系统分离的项目,前台渲染数据和后台管理系统变动的数据实时同步,那么就会用MQ消息队列实现数据同步。
ActiveMQ消息队列提供了Queue和Topic模式实现,即点对点和点对多,区别说白了就是订阅者可以有一个或多个,监听主题角色的数据,当主题数据变化就会通知订阅者实现数据同步,采用观察者模式实现。
2.2 详细描述一次 RPC 调用经过的完整链路。
答:首先,需要配置好注册中心来进行服务注册和服务治理,管理发布者发布的服务。
其次,需要提供服务生产者和消费者,总的实现原则是:生产者发布消息到注册中心,消费者订阅注册中心获取服务。
RPC远程调用最核心的部分就是通信机制,一般都是基于HTTP协议实现通信,因为HTTP协议底层是基于TCP长连接的,比较稳定和可靠。
RPC远程调用数据传输已办都使用一些常用的通用的、跨语言、跨平台数据交换格式,比如JSON、MessagePack、XML等等。
RPC远程调用可以通过客户端负载均衡和服务器负载均衡来管理调度不同的服务请求,减轻服务器的访问压力,比如客户端负载均衡ribbon、feign,服务器负载均衡nginx等
发布者通过serviceid把自己注册到注册中心,消费者在需要调用的时候就可以通过需要调用的serviceid获取注册中心对应的服务实现远程调用。
3. 开放题
3.1 线上机器 CPU、Load 飙升的原因可能有哪些?
答:
内存泄漏、内存溢出、空轮训、线程死锁
内存泄漏:JVM堆区间新生代老年代都存满了,超过了最大可用内存大小,此时服务会直接不可用,必须强制去定位GC日志释放内存后重启服务,ERROR级别异常
内存溢出:内存溢出是指内存泄漏发生之后,程序依然在往JVM中申请内存空间,导致本来已经存不下的JVM堆区间不断加入新的对象导致溢出,这种问题的体现就是会发现
FGC频繁执行,并且内存大小反而还在不断提高,意思是FGC垃圾回收不够充分,这时就要考虑是否发生了内存溢出问题,内存溢出问题可以调整JVM堆区间大小和其他的一些GC调优方式实现
空轮训:程序中存在大量的死循环获取数据,比如Selector选择器空轮训问题。
线程死锁:线程锁里面嵌套锁,可以通过gc日志deadLock定位问题代码
有一个窍门,对于使用了SpringCloud的项目而言,可以使用Hystrix、Turbin等仪表盘来实时监测服务运行情况,遇见问题可以通过Sleuth+Zipkin请求服务链路动态追踪快速定位问题。
3.2 工作过程中,经常遇到各种各样的问题,如何快速定位问题?或者描述自己排查问题的思路?
答:
首先断掉调试这个,就不用多说了吧
然后,对于目前做的SOA架构和微服务架构项目而言,我一般优先考虑一些官方的各类工具来及时发现和定位问题
其次,我会使用PostMan等接口监测工具传递一些测试参数来模拟发送请求,检查服务器代码问题,这个主要是因为前后台分离,我不可能每次都让前台同时发送请求过来吧,
一般情况下,如果是一些基本的代码常见问题、经常产生的异常问题,都可以根据堆栈信息快速解决。
如果是一些之前碰到过的问题,或者有印象但是长时间没遇到的问题,我会问同事,同时去原来的代码中找到类似的代码文件进行比对。
如果是一些没有碰到过的问题,我会直接去百度,节约时间。
如果是前后台联调的时候发现的问题,我会直接去让前台同时把浏览器调试工具获取到的参数发给我(这种情况一般是用于后台数据查询出来,但是渲染结果不对的情况,一般都是因为参数名不一致导致),
如果是后台接收参数不对的问题,我会通过一些日志工具比如logback、log4j等打印日志定位问题。
后台问题主要还是通过日志来发现、跟踪、定位问题
如果发生线上和本机运行结果不一致的问题,一般会先把线上的代码拿过来反编译,确认一致还是没有发现问题,就会远程调试
3.3 你认为系统开发过程中的难点有哪些?
答:
这种问题一方面需要有成熟的文档和API接口规范来保证前后台代码规范,一方面前后台的单元、集成测试都必须做充分
对于500系列问题,排查代码,把class文件拿过来反编译进行对比,确认有问题则改问题,如果一致,则需要检查数据库数据情况,是否有冗余数据、字典不匹配的问题等等
对于404系列问题,一方面联系前端,看接口url是否错误,一方面检查注册中心服务是否出现问题
对于400/403系列问题,检查参数接收和前台传递的是否一致