记一次面试腾讯全资子公司问到的问题
1.你项目中用到了spring cloud,请问你用了哪些spring cloud组件?答:服务发现——Netflix Eureka,客服端负载均衡——Netflix Ribbon,断路器——Netflix Hystrix,服务网关——Netflix Zuul,分布式配置——Spring Cloud Config2.谈一谈断路器Hystrix的原理答:Hystrix是一个...
1.你项目中用到了spring cloud,请问你用了哪些spring cloud组件?
答:服务发现——Netflix Eureka,客服端负载均衡——Netflix Ribbon,
断路器——Netflix Hystrix,服务网关——Netflix Zuul,分布式配置——Spring Cloud Config
2.谈一谈断路器Hystrix的原理
答:Hystrix 是一个帮助解决分布式系统交互时超时处理和容错的类库, 它同样拥有保护系统的能力。Netflix的众多开源项目之一。
a. 隔离:
Hystrix隔离方式采用线程/信号的方式,通过隔离限制依赖的并发量和阻塞扩散
1)线程隔离
Hystrix在用户请求和服务之间加入了线程池。
Hystrix为每个依赖调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。线程数是可以被设定的。
原理:用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,则会进行降级处理,用户的请求不会被阻塞,至少可以看到一个执行结果(例如返回友好的提示信息),而不是无休止的等待或者看到系统崩溃。
隔离前:
隔离后:
2)信号隔离:
信号隔离也可以用于限制并发访问,防止阻塞扩散, 与线程隔离最大不同在于执行依赖代码的线程依然是请求线程(该线程需要通过信号申请, 如果客户端是可信的且可以快速返回,可以使用信号隔离替换线程隔离,降低开销。信号量的大小可以动态调整, 线程池大小不可以。
b. 熔断:
如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
熔断器:Circuit Breaker
熔断器是位于线程池之前的组件。用户请求某一服务之后,Hystrix会先经过熔断器,此时如果熔断器的状态是打开(跳起),则说明已经熔断,这时将直接进行降级处理,不会继续将请求发到线程池。熔断器相当于在线程池之前的一层屏障。每个熔断器默认维护10个bucket ,每秒创建一个bucket ,每个blucket记录成功,失败,超时,拒绝的次数。当有新的bucket被创建时,最旧的bucket会被抛弃。
熔断器的状态机:
- Closed:熔断器关闭状态,调用失败次数积累,到了阈值(或一定比例)则启动熔断机制;
- Open:熔断器打开状态,此时对下游的调用都内部直接返回错误,不走网络,但设计了一个时钟选项,默认的时钟达到了一定时间(这个时间一般设置成平均故障处理时间,也就是MTTR),到了这个时间,进入半熔断状态;
- Half-Open:半熔断状态,允许定量的服务请求,如果调用都成功(或一定比例)则认为恢复了,关闭熔断器,否则认为还没好,又回到熔断器打开状态;
3.说一说hashmap的扩容机制,hashmap不是线程安全的,一写多读会怎么样?
答:hashmap的扩容需要满足两个条件:当前数据存储的数量(即size())大小必须大于等于阈值;当前加入的数据是否发生了hash冲突。
因为上面这两个条件,所以存在下面这些情况
(1)、就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。
(2)、当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。
(3)读写不一致也就会产生线程安全问题,所以hashmap一写多读也不是线程安全的。
4.你项目中用到了redis,说一说redis有哪些结构?
答:字符串,哈希,列表,集合,有序集合
5.redis中的存储的字符串value值有啥限制
答:最大不能超过512M。
6.谈一谈redis的list结构
答:列表对象的编码可以是ziplist和linkedlist之一。
(1) ziplist编码
ziplist编码的哈希随想底层实现是压缩列表,每个压缩里列表节点保存了一个列表元素。
(2)linkedlist编码
linkedlist编码底层采用双端链表实现,每个双端链表节点都保存了一个字符串对象,在每个字符串对象内保存了一个列表元素。
列表对象编码转换:
-
- 列表对象使用ziplist编码需要满足两个条件:一是所有字符串长度都小于64字节,二是元素数量小于512,不满足任意一个都会使用linkedlist编码。
- 两个条件的数字可以在Redis的配置文件中修改,list-max-ziplist-value选项和list-max-ziplist-entries选项。
- 图中StringObject就是上一节讲到的字符串对象,字符串对象是唯一个在五大对象中作为嵌套对象使用的
7.你项目经历还写了用到了k8s,谈一谈你对k8s的认识?
答:Kubernetes,它可以帮助用户省去应用容器化过程的许多手动部署和扩展操作。也就是说,您可以将运行 Linux 容器的多组主机聚集在一起,由 Kubernetes 帮助您轻松高效地管理这些集群。而且,这些集群可跨公共云、私有云或混合云部署主机。因此,对于要求快速扩展的云原生应用而言(例如借助 Apache Kafka 进行的实时数据流处理)Kubernetes 是理想的托管平台。。真正的生产型应用会涉及多个容器。这些容器必须跨多个服务器主机进行部署。Kubernetes 可以提供所需的编排和管理功能,以便您针对这些工作负载大规模部署容器。借助 Kubernetes 编排功能,您可以构建跨多个容器的应用服务、跨集群调度、扩展这些容器,并长期持续管理这些容器的健康状况。然后巴拉巴拉说了一些七杂八杂的。
8.谈一谈K8S中的node和pod?
除了Master,Kubernetes集群中的其他机器被称为Node节点,在较早的版本中也被称为Minion。与Master一样,Node节点可以是一台物理主机,也可以是一台虚拟机。Node节点才是Kubernetes集群中的工作负载节点,每个Node都会被Master分配一些工作负载(Docker容器),当某个Node宕机时,其上的工作负载会被Master自动转移到其他节点上去。
每个Node节点上都运行着以下一组关键进程:
kubelet:负责Pod对应的容器的创建、启停等任务,同时与Master节点密切协作,实现集群管理的基本功能。
kube-proxy:实现Kubernetes Service的通信与负载均衡机制的重要组件。
Docker Engine (docker):Docker引擎,负责本机的容器创建和管理工作。
Node节点可以在运行期间动态增加到Kubernetes集群中,前提是这个节点上已经正确安装、配置和启动了上述关键进程,在默认情况下kubelet会向Master注册自己,这也是Kubernetes推荐的Node管理方式。一旦Node被纳入集群管理范围,kubelet进程就会定时向Master节点汇报自身的情报,例如操作系统、Docker版本、机器的CPU和内存情况,以及当前有哪些Pod在运行等,这样Master可以获知每个Node的资源使用情况,并实现高效均衡等资源调度策略。而某个Node超过指定时间不上报信息时,会被Master判断为“失联”,Node的状态被标记为不可用(Not Ready),随后Master会触发“工作负载大转移”的自动流程。
Pod是Kubernetes的最重要也最基本的概念,如下图所示是Pod的组成示意图,我们看到每个Pod都有一个特殊的被成为“根容器”的Pause容器。Pause容器对应的镜像属于Kubernetes平台的一部分,除了Pause容器,每个Pod还包含一个或多个紧密相关的用户业务容器。
9.说一说K8S的常用命令?
下表包括所有kubectl操作的简短描述和一般语法:
Operation | Syntax | Description | |
annotate | `kubectl annotate (-f FILENAME | TYPE NAME | |
api-versions | kubectl api-versions [flags] | 列出可用的API版本。 | |
apply | kubectl apply -f FILENAME [flags] | 对文件或标准输入流更改资源应用配置。 | |
attach | kubectl attach POD -c CONTAINER [-i] [-t] [flags] | attach 到正在运行的容器来查看输出流或与容器(stdin)进行交互。 | |
autoscale | `kubectl autoscale (-f FILENAME | TYPE NAME | |
cluster-info | kubectl cluster-info [flags] | 显示有关集群中master节点和服务的端点信息。 | |
config | kubectl config SUBCOMMAND [flags] | 修改kubeconfig文件。有关详细信息,请参阅各个子命令。 | |
create | kubectl create -f FILENAME [flags] | 从文件或stdin创建一个或多个资源。 | |
delete | `kubectl delete (-f FILENAME | TYPE [NAME | |
describe | `kubectl describe (-f FILENAME | TYPE [NAME_PREFIX | |
edit | `kubectl edit (-f FILENAME | TYPE NAME | |
exec | kubectl exec POD [-c CONTAINER] [-i] [-t] [flags] [-- COMMAND [args...]] | 对pod中的容器执行命令 | |
explain | kubectl explain [--include-extended-apis=true] [--recursive=false] [flags] | 获取各种资源的文档。例如 pods, nodes, services 等. | |
expose | `kubectl expose (-f FILENAME | TYPE NAME | |
get | `kubectl get (-f FILENAME | TYPE [NAME | |
label | `kubectl label (-f FILENAME | TYPE NAME | |
logs | kubectl logs POD [-c CONTAINER] [--follow] [flags] | 在pod的容器中打印日志。 | |
patch | `kubectl patch (-f FILENAME | TYPE NAME | |
port-forward | kubectl port-forward POD [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N] [flags] | 将一个或多个本地端口转发到pod。 | |
proxy | kubectl proxy [--port=PORT] [--www=static-dir] [--www-prefix=prefix] [--api-prefix=prefix] [flags] | 运行一个代理到Kubernetes API服务器。 | |
replace | kubectl replace -f FILENAME | 从文件或stdin替换资源。 | |
rolling-update | `kubectl rolling-update OLD_CONTROLLER_NAME ([NEW_CONTROLLER_NAME] –image=NEW_CONTAINER_IMAGE | -f NEW_CONTROLLER_SPEC) [flags]` | |
run | kubectl run NAME --image=image [--env="key=value"] [--port=port] [--replicas=replicas] [--dry-run=bool] [--overrides=inline-json] [flags] | 在集群上运行指定的镜像。 | |
scale | `kubectl scale (-f FILENAME | TYPE NAME | |
stop | kubectl stop | 已弃用: 相应的, 请查看 kubectl delete. | |
version | kubectl version [--client] [flags] | 显示在客户端和服务器上运行的Kubernetes版本。 |
10.你简历项目中用到了kafka,谈一谈你们为什么用kafka,而不用其他消息队列?
答:
特性 MQ | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
生产者消费者模式 | 支持 | 支持 | 支持 | 支持 |
发布订阅模式 | 支持 | 支持 | 支持 | 支持 |
请求回应模式 | 支持 | 支持 | 不支持 | 不支持 |
Api完备性 | 高 | 高 | 高 | 高 |
多语言支持 | 支持 | 支持 | java | 支持 |
单机吞吐量 | 万级 | 万级 | 十万级 | 十万级 |
消息延迟 | 无 | 微秒级 | 毫秒级 | 毫秒级 |
可用性 | 高(主从) | 高(主从) | 非常高(分布式) | 非常高(分布式) |
消息丢失 | 低 | 低 | 理论上不会丢失 | 理论上不会丢失 |
文档的完备性 | 高 | 高 | 教高 | 高 |
提供快速入门 | 有 | 有 | 有 | 有 |
社区活跃度 | 高 | 高 | 中 | 高 |
商业支持 | 无 | 无 | 商业云 | 商业云 |
总体来说:
ActiveMQ 历史悠久的开源项目,已经在很多产品中得到应用,实现了JMS1.1规范,可以和spring-jms轻松融合,实现了多种协议,不够轻巧(源代码比RocketMQ多),支持持久化到数据库,对队列数较多的情况支持不好。
RabbitMQ 它比Kafka成熟,支持AMQP事务处理,在可靠性上,RabbitMQ超过Kafka,在性能方面超过ActiveMQ。
RocketMQ RocketMQ是阿里开源的消息中间件,目前在Apache孵化,使用纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是简单的复制,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景,支撑了阿里多次双十一活动。 因为是阿里内部从实践到产品的产物,因此里面很多接口、API并不是很普遍适用。其可靠性毋庸置疑,而且与Kafka一脉相承(甚至更优),性能强劲,支持海量堆积。
Kafka Kafka设计的初衷就是处理日志的,不支持AMQP事务处理,可以看做是一个日志系统,针对性很强,所以它并没有具备一个成熟MQ应该具备的特性。Kafka的性能(吞吐量、tps)比RabbitMQ要强,如果用来做大数据量的快速处理是比RabbitMQ有优势的。
11.kafka为什么比较快?
Kafka的消息是保存或缓存在磁盘上的,一般认为在磁盘上读写数据是会降低性能的,因为寻址会比较消耗时间,但是实际上,Kafka的特性之一就是高吞吐率。
即使是普通的服务器,Kafka也可以轻松支持每秒百万级的写入请求,超过了大部分的消息中间件,这种特性也使得Kafka在日志处理等海量数据场景广泛应用。
写入数据,Kafka会把收到的消息都写入到硬盘中,它绝对不会丢失数据。为了优化写入速度Kafka采用了两个技术, 顺序写入 和 MMFile 。
顺序写入,磁盘读写的快慢取决于你怎么使用它,也就是顺序读写或者随机读写。在顺序读写的情况下,某些优化场景磁盘的读写速度可以和内存持平(注:此处有疑问, 不推敲细节,参考 http://searene.me/2017/07/09/Why-is-Kafka-so-fast/
)。
因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最讨厌随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。
而且Linux对于磁盘的读写优化也比较多,包括read-ahead和write-behind,磁盘缓存等。如果在内存做这些操作的时候,一个是JAVA对象的内存开销很大,另一个是随着堆内存数据的增多,JAVA的GC时间会变得很长,使用磁盘操作有以下几个好处:
磁盘顺序读写速度超过内存随机读写
JVM的GC效率低,内存占用大。使用磁盘可以避免这一问题
系统冷启动后,磁盘缓存依然可用
上图就展示了Kafka是如何写入数据的, 每一个Partition其实都是一个文件 ,收到消息后Kafka会把数据插入到文件末尾(虚框部分)。
这种方法有一个缺陷—— 没有办法删除数据 ,所以Kafka是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个Topic都有一个offset用来表示 读取到了第几条数据 。
两个消费者,Consumer1有两个offset分别对应Partition0、Partition1(假设每一个Topic一个Partition);Consumer2有一个offset对应Partition2。这个offset是由客户端SDK负责保存的,Kafka的Broker完全无视这个东西的存在;一般情况下SDK会把它保存到zookeeper里面。(所以需要给Consumer提供zookeeper的地址)。
如果不删除硬盘肯定会被撑满,所以Kakfa提供了两种策略来删除数据。一是基于时间,二是基于partition文件大小。具体配置可以参看它的配置文档。
Memory Mapped Files
即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以Kafka的数据并 不是实时的写入硬盘 ,它充分利用了现代操作系统 分页存储 来利用内存提高I/O效率。
Memory Mapped Files(后面简称mmap)也被翻译成 内存映射文件 ,在64位操作系统中一般可以表示20G的数据文件,它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
通过mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),也不必关心内存的大小有虚拟内存为我们兜底。
使用这种方式可以获取很大的I/O提升, 省去了用户空间到内核空间 复制的开销(调用文件的read会把数据先放到内核空间的内存中,然后再复制到用户空间的内存中。)也有一个很明显的缺陷——不可靠, 写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。 Kafka提供了一个参数——producer.type来控制是不是主动flush,如果Kafka写入到mmap之后就立即flush然后再返回Producer叫 同步 (sync);写入mmap之后立即返回Producer不调用flush叫 异步 (async)。
读取数据,Kafka在读取磁盘时做了哪些优化?
基于sendfile实现Zero Copy
传统模式下,当需要对一个文件进行传输的时候,其具体流程细节如下:
调用read函数,文件数据被copy到内核缓冲区
read函数返回,文件数据从内核缓冲区copy到用户缓冲区
write函数调用,将文件数据从用户缓冲区copy到内核与socket相关的缓冲区。
数据从socket缓冲区copy到相关协议引擎。
以上细节是传统read/write方式进行网络文件传输的方式,我们可以看到,在这个过程当中,文件数据实际上是经过了四次copy操作:
硬盘—>内核buf—>用户buf—>socket相关缓冲区—>协议引擎
而sendfile系统调用则提供了一种减少以上多次copy,提升文件传输性能的方法。
在内核版本2.1中,引入了sendfile系统调用,以简化网络上和两个本地文件之间的数据传输。 sendfile的引入不仅减少了数据复制,还减少了上下文切换。
sendfile(socket, file, len);
运行流程如下:
sendfile系统调用,文件数据被copy至内核缓冲区
再从内核缓冲区copy至内核中socket相关的缓冲区
最后再socket相关的缓冲区copy到协议引擎
相较传统read/write方式,2.1版本内核引进的sendfile已经减少了内核缓冲区到user缓冲区,再由user缓冲区到socket相关缓冲区的文件copy,而在内核版本2.4之后,文件描述符结果被改变,sendfile实现了更简单的方式,再次减少了一次copy操作。
在apache,nginx,lighttpd等web服务器当中,都有一项sendfile相关的配置,使用sendfile可以大幅提升文件传输性能。
Kafka把所有的消息都存放在一个一个的文件中,当消费者需要数据的时候Kafka直接把文件发送给消费者,配合mmap作为文件读写方式,直接把它传给sendfile。
批量压缩
在很多情况下,系统的瓶颈不是CPU或磁盘,而是网络IO,对于需要在广域网上的数据中心之间发送消息的数据流水线尤其如此。进行数据压缩会消耗少量的CPU资源,不过对于kafka而言,网络IO更应该需要考虑。
如果每个消息都压缩,但是压缩率相对很低,所以Kafka使用了批量压缩,即将多个消息一起压缩而不是单个消息压缩
Kafka允许使用递归的消息集合,批量的消息可以通过压缩的形式传输并且在日志中也可以保持压缩格式,直到被消费者解压缩
Kafka支持多种压缩协议,包括Gzip和Snappy压缩协议
Kafka速度的秘诀在于,它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,通过mmap提高I/O速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。
12.你项目中用到了多线程,如果线上发生死锁问题,你会怎么排查?
答:在分布式部署的情况下需要根据链路追踪找到对应服务器死锁的进程,找到机器后根据jps -l 查找正在运行的java程序的pid,然后再根据jastack -pid 就能查到死锁的位置
总结:
这是今晚进行一场电话面试实际问到的问题,问的问题基本平时都遇到过,但感觉回答得不够深入,所以面试了大概40分钟就结束了。面而知不足,现整理下来记录(答案已做整理)
更多推荐
所有评论(0)