Java岗大厂面试百日冲刺【Day38】—— 实战那些事儿2
Java岗大厂面试百日冲刺【Day38】—— 实战那些事儿2
本文已获得原作者 _陈哈哈 授权并经过重新整理规划后发布。
本栏目Java开发岗高频面试题主要出自以下各技术栈:Java基础知识、集合容器、并发编程、JVM、Spring全家桶、MyBatis等ORMapping框架、MySQL数据库、Redis缓存、RabbitMQ消息队列、Linux操作技巧等。
面试题1:当你发现一条SQL很慢,你的处理思路是什么?
我们是如何发现慢SQL的?除了慢查询日志和人为发现之外,一般出现慢查询时会有如下三个特征:
- 数据库CPU负载高。一般是查询语句中有很多计算逻辑,或并发处理线程较多,导致数据库cpu负载。
- IO过高导致服务器卡住,这个一般和全表查询没索引有关系,问题出在处理的数据量太大。
- 查询语句正常,索引正常但是还是慢。如果表面上索引都配置了,但是查询慢,那得看看索引是否生效。
有些SQL虽然出现在慢查询日志中,但未必是其本身的性能问题,可能是因为锁等待,服务器压力高等等。
需要分析SQL语句真实的执行计划,而不是看重新执行一遍SQL时,看是不是变快了(查询缓存都不带考虑的?)。。而是使用Explain工具来逐步调优,了解 MySQL 在执行这条数据时的一些细节,比如是否进行了优化、是否使用了索引、索引选择器是否正确选择等等。基于 Explain 的返回结果我们就可以根据 MySQL 的执行细节进一步优化语法,使索引能被正确使用,实现性能需求。
关于索引的创建及优化原则,美团点评技术团队的几点总结,引用一下:
- 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整;
- =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式,当然,好习惯要靠自己保持;
- 尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录;
- 索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);
- 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
当然,我们知道还有单表数据量过大、大字段检索、模糊匹配效率、时间维度统计效果差等MySQL硬性问题,俗称硬伤,那我们就得基于实际情况来进行分库分表、切换检索引擎(如ES、FastDFS)等方式来处理了,术业有专攻,总不能在一棵树上吊死对吧。
面试题2:怎么理解负载均衡的?你处理负载均衡都有哪些途径?
负载均衡(Load Balance)是集群技术(Cluster)的一种应用。负载均衡可以将工作任务分摊到多个处理单元,从而提高并发处理能力。下面是一组普通的web架构,我理解做负载均衡的切入点一般在web层和数据层。
目前最常见的负载均衡应用是Web层负载均衡。根据实现的原理不同,常见的web负载均衡技术包括:DNS轮询、IP负载均衡、CDN、反向代理以及http重定向等等。其中IP负载均衡可以使用硬件设备或软件方式来实现。
对于数据层负载均衡,我们常用的分布式架构、多节点集群、消息中间件(如Mycat)等来控制集群负载均衡状况,同样是横向扩展,但侧重点在于对集群的管理和灾备(高可用)方面。因此,实际大多负载均衡的工作还是在Web层。
- 高性能集群:将单个重负载的请求分散到多个节点进行处理,最后再将处理结果进行汇总;
- 高可用集群:提高冗余单元,避免单点故障;
- 负载均衡集群:将大量的并发请求分担到多个处理节点。由于单个处理节点的故障不影响整个服务,负载均衡集群同时也实现了高可用性。
Web层负载均衡
任何的负载均衡技术都要想办法建立某种一对多的映射机制:一个请求的入口映射到多个处理请求的节点,从而实现分而治之(Divide and Conquer)。
1、【协议层】http重定向
当http代理(比如浏览器)向web服务器请求某个URL后,web服务器可以通过http响应头信息中的Location标记来返回一个新的URL。这意味着HTTP代理需要继续请求这个新的URL,完成自动跳转。
这种负载均衡方案的有点是比较简单,缺点是浏览器需要两次请求服务器才能完成一次访问,性能较差;同时,重定向服务器本身的处理能力有可能成为瓶颈,整个集群的伸缩性规模有限;使用HTTP返回码302重定向,有可能使搜索引擎判断为SEO作弊,降低搜索排名。因此实践中很少使用这种负载均衡方案来部署。
2、【协议层】DNS轮询
DNS负责提供域名解析服务,当访问某个站点时,实际上首先需要通过该站点域名的DNS服务器来获取域名指向的IP地址,在这一过程中,DNS服务器完成了域名到IP地址的映射,同样,这样映射也可以是一对多的,这时候,DNS服务器便充当了负载均衡调度器,它就像http重定向转换策略一样,将用户的请求分散到多台服务器上,但是它俩的实现机制完全不同。
相比http重定向,基于DNS的负载均衡完全节省了所谓的主站点,或者说DNS服务器已经充当了主站点的职能。但不同的是,作为调度器,DNS服务器本身的性能几乎不用担心。因为DNS记录可以被用户浏览器或者互联网接入服务商的各级DNS服务器缓存,只有当缓存过期后才会重新向域名的DNS服务器请求解析。也说是DNS不存在http的吞吐率限制,理论上可以无限增加实际服务器的数量。
优势:
- 可以根据用户IP来进行智能解析。DNS服务器可以在所有可用的A记录中寻找离用记最近的一台服务器。
- 动态DNS:在每次IP地址变更时,及时更新DNS服务器。当然,因为缓存,一定的延迟不可避免。
不足:
- 没有用户能直接看到DNS解析到了哪一台实际服务器,对运维人员的调试带来了不便。
- 策略的局限性。例如你无法将HTTP请求的上下文引入到调度策略中,而在前面介绍的基于HTTP重定向的负载均衡系统中,调度器工作在HTTP层面,它可以充分理解HTTP请求后根据站点的应用逻辑来设计调度策略,比如根据请求不同的URL来进行合理的过滤和转移。
3、【协议层】CDN
CDN(Content Delivery Network,内容分发网络)。通过发布机制将内容同步到大量的缓存节点,并在DNS服务器上进行扩展,找到里用户最近的缓存节点作为服务提供节点。
因为很难自建大量的缓存节点,所以通常使用CDN运营商的服务。目前国内的服务商很少,而且按流量计费,价格昂贵。
4、【协议层】反向代理负载均衡
在客户端明确的前提下,大量访问请求(QPS)涌入。我们后台通过Nginx代理了20个服务器,高QPS打进来后先打到Nginx中,通过Nginx的负载均衡来把请求分发给这20台服务器,减轻了单台服务器负担。这种客户端 → Nginx → 服务器 的模式称为反向代理,如下图:
N个客户端给服务器发送的请求,Nginx服务器接收到之后,按照一定的规则均衡分发给了后端的业务处理服务器进行处理了。此时,请求的客户端是明确的,但是请求具体由哪台服务器处理的并不明确了,Nginx扮演的就是一个反向代理角色。
- 客户端是无感知代理的存在的,反向代理对外都是透明的,访问者并不知道自己访问的是一个代理。因为客户端不需要任何配置就可以访问。
- 反向代理,它代理的是服务端,代服务端接收请求,主要用于服务器集群分布式部署的情况下,反向代理隐藏了服务器的信息。
反向代理的作用:
(1)保证内网的安全,通常将反向代理服务器配置为公网访问地址,代理的Web服务器是内网IP。
(2)负载均衡,通过反向代理服务器来优化每个单机服务实例的负载。
5、【网络层】IP负载均衡
- 客户端会向一个ip地址发出请求,这个ip地址是一个VIP(虚拟IP),这也是调度器向外公布的一个地址。
- 请求达到调度器,调度器会根据负载均衡算法从RealServer列表中选取一个负载不高的服务器,然后把请求报文的目标地址,也就是VIP和端口通过iptables进行NAT转换成选中的服务器的真实ip地址。最后,调度器会把其连接保存在一个hash表中,只要这个连接下次再发请求报文过来就会把其分发到上次选定的服务器中。
- RealServer收到报文之后,会把响应返回给调度器。
- 调度器收到报文之后,会把源地址和源端口改为虚拟ip和端口,最后再返回给客户端。
特点
- RealServer和调度器必须位于一个ip网络之中。
- 调度器位于RealServer和客户端之间,处理进出的通信。
- RIP通常是内部地址,仅用于集群之间通信。
- RealServer的网关必须指向调度器。
- 支持端口映射,RealServer没必要跟调度器一个端口。
限制
响应报文一般比较大,每一次都需要NAT转换的话,大流量的时候,会导致调度器成为一个瓶颈。
面试题3:你平时是怎样定位线上问题的?
本题回答摘自朱晔的《Java业务开发常见错误100例》
定位问题,首先要定位问题出在哪个层次上。比如,是 Java 应用程序自身的问题还是外部因素导致的问题。我们可以先查看程序是否有异常,异常信息一般比较具体,可以马上定位到大概的问题方向;如果是一些资源消耗型的问题可能不会有异常,我们可以通过指标监控配合显性问题点来定位。
一般情况下,程序的问题来自以下三个方面。
第一,程序发布后的 Bug,回滚后可以立即解决。这类问题的排查,可以回滚后再慢慢分析版本差异。
第二,外部因素,比如主机、中间件或数据库的问题。这类问题的排查方式,按照主机层面的问题、中间件或存储(统称组件)的问题分为两类。
主机层面的问题,可以使用工具排查:
- CPU 相关问题:可以使用 top、vmstat、pidstat、ps 等工具排查;
- 内存相关问题:可以使用 free、top、ps、vmstat、cachestat、sar 等工具排查;
- IO 相关问题:可以使用 lsof、iostat、pidstat、sar、iotop、df、du 等工具排查;
- 网络相关问题:可以使用 ifconfig、ip、nslookup、dig、ping、tcpdump、iptables 等工具排查。
组件的问题,可以从以下几个方面排查:
- 排查组件所在主机是否有问题;
- 排查组件进程基本情况,观察各种监控指标;
- 查看组件的日志输出,特别是错误日志;
- 进入组件控制台,使用一些命令查看其运作情况。
第三,因为系统资源不够造成系统假死的问题,通常需要先通过重启和扩容解决问题,之后再进行分析,不过最好能留一个节点作为现场。系统资源不够,一般体现在 CPU 使用高、内存泄漏或 OOM 的问题、IO 问题、网络相关问题这四个方面。
对于 CPU 使用高的问题,如果现场还在,具体的分析流程是:
- 首先,在 Linux 服务器上运行top -Hp pid命令,来查看进程中哪个线程 CPU 使用高;
- 然后,输入大写的 P 将线程按照 CPU 使用率排序,并把明显占用 CPU 的线程 ID 转换为 16 进制;
- 最后,在 jstack 命令输出的线程栈中搜索这个线程 ID,定位出问题的线程当时的调用栈。
如果没有条件直接在服务器上运行 top 命令的话,我们可以用采样的方式定位问题:间隔固定秒数(比如 10 秒)运行一次 jstack 命令,采样几次后,对比采样得出哪些线程始终处于运行状态,分析出问题的线程。
如果现场没有了,我们可以通过排除法来分析。CPU 使用高,一般是由下面的因素引起的:
- 突发压力。这类问题,我们可以通过应用之前的负载均衡的流量或日志量来确认,诸如 Nginx 等反向代理都会记录 URL,可以依靠代理的 Access Log 进行细化定位,也可以通过监控观察 JVM 线程数的情况。压力问题导致 CPU 使用高的情况下,如果程序的各资源使用没有明显不正常,之后可以通过压测 +Profiler(jvisualvm 就有这个功能)进一步定位热点方法;如果资源使用不正常,比如产生了几千个线程,就需要考虑调参。
- GC。这种情况,我们可以通过 JVM 监控 GC 相关指标、GC Log 进行确认。如果确认是 GC 的压力,那么内存使用也很可能会不正常,需要按照内存问题分析流程做进一步分析。
- 程序中死循环逻辑或不正常的处理流程。这类问题,我们可以结合应用日志分析。一般情况下,应用执行过程中都会产生一些日志,可以重点关注日志量异常部分。
对于内存泄露或 OOM 的问题,最简单的分析方式,就是堆转储后使用MAT分析。堆转储,包含了堆现场全貌和线程栈信息,一般观察支配树图、直方图就可以马上看到占用大量内存的对象,可以快速定位到内存相关问题。
需要注意的是,Java 进程对内存的使用不仅仅是堆区,还包括线程使用的内存(线程个数 * 每一个线程的线程栈)和元数据区。每一个内存区都可能产生 OOM,可以结合监控观察线程数、已加载类数量等指标分析。另外,我们需要注意看一下,JVM 参数的设置是否有明显不合理的地方,限制了资源使用。
问题来了,JVM 哪块内存区域不会发生内存溢出?
程序计数器:程序计数器是一块内存较小的区域,它用于存储线程的每个执行指令,每个线程都有自己的程序计数器,此区域不会有内存溢出的情况。
IO 相关的问题,除非是代码问题引起的资源不释放等问题,否则通常都不是由 Java 进程内部因素引发的。网络相关的问题,一般也是由外部因素引起的。
对于连通性问题,结合异常信息通常比较容易定位;对于性能或瞬断问题,可以先尝试使用 ping 等工具简单判断,如果不行再使用 tcpdump 或 Wireshark 来分析。
- 感谢你赐予我前进的力量