开发岗面试汇总

目录

中间件:

Redis:

支持的数据结构和底层实现:

REDIS主从实现原理:

分块分表:

redis怎么实现高并发:

redis持久化机制:

redis缓存雪崩,缓存穿透,缓存击穿,如何解决雪崩问题:

Redis过期策略:

Redis的淘汰策略:

Redis分布式锁的实现:

如何解决redis分布式锁过期时间到了业务没执行完问题:

Redis重新分配哈希值:

Redis快的原因:

redis twemProxy:

高可用:

Redis分布式锁:

Kafka:

是什么:

partition的数据文件:

数据文件分段segment:

负载均衡:

批量发送:

压缩:

Consumer Group:

如何获取topic主题的列表:

zk对于kafka的作用是什么:

kafka判断一个节点是否还活着有哪两个条件:

讲一讲kafka的ack的三种机制:

如何控制消费的位置:

kafka分布式的情况下,如何保证消息的顺序消费:

kafka的高可用:

kafka的rebalance机制:

MySQL:

mysql事务隔离级别:

索引原理、使用优化方式:

为什么添加了索引,查询会更快:

InnoDB存储引擎的特点:

锁:

大表优化:

Nginx:

nginx的特点:

Nginx为什么快:

负载均衡算法:

Gin:

gorm是如何实现的:

gin的handler底层数据结构:

GRPC相对于RESTFUL有什么优势:

Django:

Django、Flask、Tornado对比:

什么是wsgi、uwsgi、uWSGI:

列举django中间的5个方法,以及django中间件的应用场景:

Django请求生命周期:

什么是CSRF,请描述其攻击原理,在Django中如何解决:

请简述Django下的(内建)缓存机制:

查询集返回的列表过滤器有哪些:

路由层:

视图层:

Celery:

Flask:

简述flask上下文管理流程:

Flask中多app应用时怎么完成:

Flask框架默认session的处理机制:

Flask和Django最大的区别:

eventlet:

Tornado:

DEVOPS:

CI/CD:

gitflow和gitlab flow:

灰度发布:

Jenkins Pipeline:

监控:

zabbix有哪些组件:

一个监控系统的运行流程:

Flink的核心概念:

Flink窗口:

水位线(watermark):

日志:

filebeat源码分析-启动:

filebeat源码解析-配置文件解析:

filebeat源码解析-采集文件扫描:

filebeat源码解析4-发送过程:

kibana API:

ES中的倒排索引是什么:

ES是如何实现master选举的:

详细描述以下ES索引文档的过程:

详细描述一下ES更新和删除文档的过程:

详细描述一下ES搜索的过程:

索引是什么:

请解释什么是分片:

什么是副本,他的作用是什么:

在ES集群中增加和创建索引的步骤是什么:

ES支持哪些类型的查询:

Elasticsearch在部署时,对Linux的设置有哪些优化方法:

ElasticSearch中的分析器是什么:

在并发情况下,ES如何保证读写一致:

ES对于大数据量(上亿量级)的聚合如何实现:

对于GC方面,在使用ES时要注意什么:

容器技术:

namespace设置的意图是什么:

每个namespace需要包含什么:

如何在Linux网络协议栈的基础上支持这些私有的独立协议栈:

不同的ns网络如何互相通信:

怎么查看对端的veth设备:

docker支持几种网络模式:

docker bridge模式如何支持网络的:

docker网络模型的局限是什么:

Namespace:

docker镜像元数据:

cgroup:

UnionFS:

rootfs是什么:

overlay2是什么:

docker文件的结构是什么:

Docker启动一个容器的过程:

Kubelet创建容器:

K8s网络模型:

CNI网络模型:

Flannel:

容器监控:

k8s存储模型:

Pod:

Service:

ConfigMap:

API Server:

Scheduler:

Controller实现高可用:

运维:

网络:

OSI七层协议、IP是哪一层、TCP是哪一层、TCP得冲突和数据链路层冲突区别:

http2.0特点:

tcp长短连接应用场景:

tcp挥手中time-wait为什么是两倍时间:

常用的网络工具:ping,tcpdump,mtr,dig:

lvs工作模式:

linux权限:


中间件:

Redis:

高性能k-v数据库,在内存运行,也可以持久化到磁盘,单线程单进程KV数据库。

支持的数据结构和底层实现:

类型 用法 实现
String set key value 最大512M
Hash(k-v集合) hmset name key1 val1 key2 val2 适合存储对象
List(有序列表,按插入顺序排序) lpush/rpush name value
SET(无序集合) sadd name value
zset(有序集合) zadd name score value每个元素都会关联一个分数,按分数排序

hash是一个string类型的field和value的映射表。它的添加、删除操作都是o(1),hash特别适合存储对象,相较于将对象的每个字段存成单个string类型。

REDIS主从实现原理:

主节点负责写,从节点负责读。

从节点发现主节点信息,建立socket链接,主节点将所有数据发送给从节点,完成初次全量同步,如果master有写命令,进行命令传播,保持主从一致性。

slave断开重连会进行同步,判断master id是否跟自己记录的一样,如果不一样就进行完整同步,如果一样就判断主从偏移量是否一致,如果不一致就进行部分重新同步,如果一样就直接结束。

分块分表:

分区是分割数据到多个redis实例的处理过程,因此每个实例保存key的一个子集。

如果只使用一个redis实例时,其中保存了服务器中全部的缓存数据,这样会有很大风险,如果单台redis服务宕机了将会影响到整个服务。解决的方法就是采用分片/分区的技术,将原来一台服务器维护的整个缓存,现在换为由多台服务器共同维护内存空间。

Redis Cluster 使用分片机制,在内部分为16384各slot插槽,分布在所有的master节点上,每个master节点负责一部分slot,数据操作时通过crc16的hash函数来对key进行取模,将结果路由到预先分配过slot相应节点上。

redis怎么实现高并发:

1、Redis是基于内存的,内存的读写速度非常快;

2、Redis是单线程的,省去很多上下文切换线程的时间;

3、Redis使用多路复用,可以处理并发的连接,非阻塞IO内部实现epoll,采用epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化为事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。

redis持久化机制:

redis一般把数据缓存在内存里,但是会周期性的把更新的数据写入磁盘

两种持久化策略:

RDB:快照形式,直接把内存dump,Redis需要做持久化时,会fork一个子进程,子进程将数据写到临时RDB表里,写完再替换,实现COW;适合冷备,每5分钟生成一次;

AOF:把对服务器修改的命令都放到文件里;每一个写命令都追加到aof文件里,redis重启时会直接根据AOF指定进行恢复,适合热备,1秒1次fsync。

redis缓存雪崩,缓存穿透,缓存击穿,如何解决雪崩问题:

缓存雪崩:redis宕机或者大量key失效,同时又有大量请求打进来,DB撑不住;

1、设置不同的过期时间,防止同时失效

2、搭建高可用redis集群

3、设置多级缓存

缓存击穿:一个key非常热点,扛大量请求,然后再某一个时间点过期,恰好这个时间点对这个key有大量请求过来,这些请求发现缓存过期时会从后台DB加载数据并回到缓存,把DB压垮。

解法:热点数据永不过期(物理不过期,逻辑不过期;当发现要过期了通过后台异步进程进行缓存重建)或者加上互斥锁。

缓存穿透:用户大量请求在缓存和DB中查不到,导致数据不存在也会大量查DB

解法:DB查不到也写在缓存里,或使用布隆过滤器(类似一个hashset,可以快速查找某个KEY是否在数据库存在)。

Redis过期策略:

1、定期删除:100ms随机抽一些设置了过期时间的删除

2、惰性删除:等查询了再看过期没,过期了就删除

Redis的淘汰策略:

设置过期时间:LRU:最近最少使用,TTL:淘汰剩余时间短的;Random:随机淘汰。

所有K-V:Random随机淘汰。

Redis分布式锁的实现:

假设有5个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在5个节点上请求锁,请求锁的超时时间应小于锁自动释放的时间。当在3个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。如果没有获取到锁,则把部分已锁的redis释放掉。

如何解决redis分布式锁过期时间到了业务没执行完问题:

思路一:任务执行的时候,开辟一个守护线程,在守护线程中每隔一段时间重新设置过期时间。

思路二:通过Redisson中的看门狗来实现,在Redisson实例被关闭前,不断延长锁的有效期,默认30秒。

Redis重新分配哈希值:

系统自动化当前各主节点中拿出一定数量的哈希槽,转义至新的主节点中,以达到哈希槽平衡。

Redis快的原因:

基于内存操作,数据结构简单,避免上下文切换和竞争关系,多路复用和非阻塞IO:使用多路复用来监听多个socket连接客户端。

redis twemProxy:

快速,轻量级,几乎无损的性能,支持代理到多个服务器和多个服务器池。

1)解耦:twemproxy是服务端分片,cluster需要客户端分片,客户端分片提高了业务对于使用redis的复杂性;

2)减少数据丢失的问题:由于网络分区或其他故障会导致cluster脑裂问题,如果客户端感知不到这种情况,在发生故障期间写入的数据在切换之后会丢失;而twemproxy可以禁用节点或者返回错误,让客户端感知。

3)减少redis直连:由于单线程模型,建立连接需要损耗性能,cluster模式直连master使用大量文件描述符,每个连接也需要占用内存,而twemproxy维持了到后端master的长连接,同时由于可以水平扩展,n各proxy可以最多建立(n * limit)个连接,极大的提高了连接数

4)通信开销小:针对大集群,cluster内部通信开销大,twemproxy管里的主从互相之间不感知,分片由proxy完成,通信开销小。

高可用:

主从复制+哨兵模式(3个)

哨兵集群:集群监控:监控Master/Slave是否正常工作;消息通知:某个Redis故障,报警给管理员;master node挂了自动转移给slave;配置中心:通知client新的master。

通过Sentine哨兵来监控Redis主服务器的状态。当主挂掉时,在从节点中根据一定策略选出新主,并调整其他从slave到新主。

选主的的策略简单来说有三个:

1、slave的priority设置的越低,优先级越高;

2、同等情况下,slave复制的数据越多优先级越高;

3、相同的条件下runid越小越容易被选中。

在Redis集群中,sentinel也会进行多实例部署,sentinel之间通过Raft协议来保证自身的高可用。

Redis分布式锁:

对于分布式系统,保证线程安全可以用Redis。竞争锁setnx:1、设置锁的有效期;2、分配唯一标示-> 访问共享资源 -> 释放锁:1、有效期结束自动释放锁;2、需要根据唯一标识来判断是否有权限删除锁,如果有则删除锁。

Kafka:

是什么:

kafka是一个高吞吐量、分布式、基于发布、订阅的消息系统。

1、broker:kafka服务器,负责消息存储和转发

2、topic:消息类别,kafka按照topic来分类消息

3、partition:topic的分区,一个topic可以包含多个partition、topic消息保存在各个partition上。

4、offset:消息在日志中的位置,可以理解是消息在partition的偏移量,也代表改消息的唯一编号。

5、producer:消息生产者

6、consumer:消息消费者

7、consumer group:消费者分组,每个consumer必须属于一个topic。

8、zookeeper:保存着broker、topic、partition的meta数据;另外,还负责broker故障发现,partition leader选举,负载均衡等。

partition的数据文件:

partition中的每条Message包含了以下三个属性:offset、messageSize、data,其中offset表示Message在这个partition的偏移量,offset不是该Message在partition数据文件中的实际存储位置,而是逻辑上一个值,它唯一确定的了partition中的一条Message,可以认为offset是partition中Message的id;MessageSize表示消息内容data的大小;data为Message的具体内容。

数据文件分段segment:

kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为index,index文件中并没有为数据文件中的每条Message建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。

负载均衡:

由于消息topic由多个partition组成,且partition会均衡分布不同broker上。因此,为了有效利用broker集群的性能,提高消息的吞吐量,producer可以随机或者hash等方式,将消息平均发送到多个partition上,以实现负载均衡。

批量发送:

是提高消息吞吐重要的方式,Producer端可以在内存中合并多条消息后,以一次请求的方式发送了批量的消息给broker,从而大大减少broker存储消息的IO操作次数。但也一定程度影响了消息的实时性,相当于以时延代价,换取更好的吞吐量。

压缩:

Producer端可以通过GZIP或Snappy格式对消息集合进行压缩。Producer端进行压缩之后,在Consumer端需要进行解压。压缩的好处就是减少传输的数据量,减轻对网络传输的压力,在对大数据处理上,瓶颈往往体现在网络上而不是CPU。

Consumer Group:

同一Comsumer Group中的多个Consumer实例,不同时消费同一个partition,等效于队列模式。partition内消息时有序的,Consumer通过pull方式消费消息。kafka不删除已消费的消息对于partition,顺序读取磁盘数据,以时间复杂度O(1)方式提供消息持久化能力。

如何获取topic主题的列表:

bin/kafka-topics.sh --list --zookeeper localhost:2181

zk对于kafka的作用是什么:

zk是一个开放源码的,高性能的协调服务,它用于kafka的分布式应用。zk主要用于在集群中不同节点之间进行通信在kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,他都可以从之前提交的偏移量中获取除此之外,它还执行其他活动,如:leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。

kafka判断一个节点是否还活着有哪两个条件:

(1)节点必须可以维护和zk的连接,zk通过心跳机制检查每个节点的连接。

(2)如果节点是follower,他必须能及时的同步leader的写操作,延时不能太久。

讲一讲kafka的ack的三种机制:

request.required.acks有三个值0,1,-1(all)

0:生产者不会等待broker的ack,这个延迟最低但是存储的保证最弱当server挂掉的时候就会丢数据。

1:服务端会等待ack值leader副本确认接收到消息后发送ack但如果leader挂掉后他不确保是否复制完成新leader也会导致数据丢失。

-1(all):服务端会等所有的follower副本收到后才会收到leader发出的ack,这样数据不会丢失。

如何控制消费的位置:

kafka使用seek(TopicParitition, long)指定新的消费位置,用于查找服务器保留的最早和最新的offset的特殊的方法也可用(seekToBegining(Collection)和seekToEnd(Collection))。

kafka分布式的情况下,如何保证消息的顺序消费:

kafka分布式的单位是partition,同一个partition用一个write ahead log组织,所以可以保证FIFO的顺序。不同partition之间不能保证顺序。但是绝大多数用户都可以通过message key来定义,因为同一个key的Message可以保证只发送到同一个partition。

kafka中发送1条消息的时候,可以指定(topic, partition, key)3个参数。partition和key是可选的,如果你指定partition,那就是所有消息发送同1个partiion,就是有序的。

并且在消费端,kafka保证,1个partition只能被1个Consumer消费,或者你指定key(比如order id),具有同1个key的所有消息,会发往同1个partition。

kafka的高可用:

实现高可用的方式一般都是进行replication,kafka选取ISR同步副本。在所有的Replica中,leader会维护一个与其基本保持同步的Replica列表,该列表成为ISR,每个Partition都会有一个ISR,而且是由leader动态维护。如果一个replica诺后leader太多,leader会将其剔除。如果另外的replica跟上脚本,leader会将其加入。同步:leader向ISR中的所有replica同步消息,当收到所有ISR中replica的ack之后,leader才会commit。异步:收到同步消息的ISR中的replica,异步将消息同步给ISR集合外的replica。

kafka的rebalance机制:

rebalance中文含义为再平衡,它规定了一个consumer group是如何达成一致来分配订阅topic的所有分区。rebalance是相对于consumer group而言,每个consumer group会从kafka broker中选出一个作为组协助者(group coordinator)。coordinator负责对整个consumer group的状态进行管理,当触发rebalance的条件发生时,促使生成新的分区分配方案。

rebalance触发的条件有三个:

consumer group成员发生变更,比如有新的consumer实例加入,或者consumer实例离开组。

consumer group订阅的topic发生变更。

consumer group订阅的topic分区数发生变化。

MySQL:

mysql事务隔离级别:

脏读:读到其他事务未提交的数据。

可重复读(UPDATE):在一个事务内,最开始读的数据和事务结束前任何时刻读到的同一批数据都是一致的。

不可重复读(UPDATE):不同时刻读到同一批数据可能是不一样的。

幻读(INSERT):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读该范围的数据行时,会发现有新的”幻影“行。

第1级别:Read Uncommitted(读取未提交内容)

(1)所有事务都可以看到其他未提交事务的执行结果(2)本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少(3)该级别引发的问题是——脏读:读取到了未提交的数据。

第2级别:Read Committed(读取提交内容)

(1)这是大多数数据库的默认隔离级别(但不是MySQL默认的)(2)它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变(3)这种隔离级别出现的问题是——不可重复读:不可重复读意味着我们在同一个事务中执行完全相同的select时可能有不一样的结果。导致这种情况的原因可能有:(1)有一个交叉的事务有新的commit,导致了数据的改变;(2)一个数据库被多个实例操作时,同一事务的其他实例在该实例处理期间可能会有新的commit。

第3级别:Repeatable Read(可重读)

(1)这是MySQL的默认事务隔离级别(2)它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行(3)此级别可能出现的问题——幻读:当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发生新的”幻影“行(4)InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

第4级别:Serializable(可串行化)

(1)这是最高的隔离级别(2)它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。(3)在这个级别,可能导致大量的超时现象和锁竞争。

索引原理、使用优化方式:

索引其实是一种数据结构,能够帮助我们快速的检索数据库中的数据。常见的MySQL主要有两种结构:Hash索引和B+Tree索引。

因为Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。所以,对于区间查询时无法直接通过索引查询的。

哈希索引没办法利用索引完成排序,如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题。

而B+树时一种多路平衡查询树,所以他的节点时天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描。

为什么添加了索引,查询会更快:

传统的查询方法,是按照表的顺序遍历的,不论插叙几条数据,mysql需要将表从头到尾遍历一遍 在我们添加完索引之后,mysql一般通过BTREE算法生成一个索引文件,在查询数据库时,找到索引文件进行遍历(折半查找大幅提升查询效率),找到相应的键从而获取数据。

索引的代价:创建索引是会产生索引文件的,占用磁盘空间;索引文件是一个二叉树类型的文件,可想而知我们的dml操作同样也会对索引文件进行修改,所以性能下降。

InnoDB存储引擎的特点:

特性一:事务性存储引擎及两个特殊日志类型:Redo Log和Undo Log

1、Innodb是一种事务性存储引擎。

2、完全支持事务的ACID特性。3、支持事务所需要的两个特殊日志类型:RedoLog和UndoLog。

MySQL的日志可以分为错误日志、二进制文件、查询日志和满查询日志。

  • 错误日志 很好理解,就是服务运行过程中发生的严重错误日志。当我们的数据库无法启动时,就可以来这里看看具体不能启动的原因是什么

  • 二进制文件 它有另外一个名字你应该熟悉,叫Binlog,其记录了对数据库所有的更改。

  • 查询日志 记录了来自客户端的所有语句

  • 慢查询日志 这里记录了所有响应时间超过阈值的SQL语句,这个阈值我们可以自己设置,参数为long_query_time,其默认值为10s,且默认是关闭的状态,需要手动的打开。

Binlog中记录了所有对数据库的修改,其记录日志有三种格式。分别是Statement、Row和MixedLevel。

  • Statement 记录所有会修改数据的SQL,其只会记录SQL,并不需要记录下这个SQL影响的所有行,减少了日志量,提高了性能。但是由于只是记录执行语句,不能保证在Slave节点上能够正确执行,所以还需要额外的记录一些上下文信息

  • Row 只保存被修改的记录,与Statement只记录执行SQL来比较,Row会产生大量的日志。但是Row不用记录上下文信息了,只需要关注被改成啥样就行。

  • MixedLevel 就是Statement和Row混合使用。

具体使用哪种日志,需要根据实际情况来决定。例如一条UPDATE语句更新了很多的数据,采用Statement会更加节省空间,但是相对的,Row会更加的可靠。

RedoLog:完成事务的持久性(已提交的事务)。UndoLog:未提交的事务,独立于表空间,需要随机访问,可以存储在高性能io设备上。

Undo日志记录某数据被修改前的值,可以用来在事务失败时进行rollback;Redo日志记录某数据被修改后的值,可以用来恢复未写入data file的已成功事务更新的数据。

InnoDB的内存架构主要分为三大块,缓冲池、重做缓冲池和额外内存池。

InnoDB为了数据的持久化,会将数据存储到磁盘上。但是面对大量的请求时,CPU的处理速度和磁盘的IO速度之间差距太大,为了提高整体的效率,InnoDB引入了缓冲池。

缓冲池说白了就是把磁盘数据丢到内存,那既然是内存就会存在没有内存空间可以分配的情况。所以缓冲池采用LRU算法,在缓冲池中没有空闲的页时,来进行页的淘汰。MYSQL采用日志先行,在真正写数据之前,会首先记录一个日志,叫Redo Log,会定期的使用CheckPoint技术将Redo Log刷入磁盘。

二次写保障可靠性:在刷脏页时,并不是直接刷入磁盘,而是copy到内存中的Doublewrite Buffer中,然后再拷贝至磁盘共享表空间(你可以就理解为磁盘)中,每次写入1M,等copy完成后,再将Doublewrite Buffer中的页写入磁盘文件。

页:是InnoDB中数据管理的最小单位。当我们查询数据时,其是以页为单位,将磁盘中的数据加载到缓冲池中的。同理,更新数据也是以页为单位,将我们对数据的修改刷回磁盘。每页的默认大小为16k,每页中包含了若干行的数据。每一页的数据,可以通过FileHeader中的上一下和下一页的数据,页与页之间可以形成双向链表。因为在实际的物理存储上,数据并不是连续存储的。你可以把他理解成G1的Region在内存中的分布。而一页中所包含的行数据,行与行之间则形成了单向链表。我们存入的行数据最终会到User Records中,当然最初User Records并不占据任何存储空间。随着我们存入的数据越来越多,User Records会越来越大,Free Space的空间会越来越小,直到被占用完,就会申请新的数据页。

锁:

锁的主要作用是管理共享资源的并发访问,锁用于实现事务的隔离性。

锁类型:共享锁、独占锁。

MySQL的事务支持与存储引擎有关:表级锁,行级锁。

1、锁的开销越大,粒度越小,并发度越高;2、表级锁是在服务器层实现的;3、行级锁是存储引擎层实现的。

阻塞是由于资源不足引起的排队等待现象;死锁是由于两个对象在拥有一份资源的情况下申请另一份资源,而另一份资源恰好又是这两对象正持有的,导致两对象无法完成操作,且所持资源无法释放。

悲观锁是基于一种悲观的态度类来防止一切数据冲突,可以保证数据的独占性和正确性,但是加锁解锁的过程会造成消耗,所以性能不高。

乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁,只有提交数据的时候才通过一种机制来验证数据是否冲突。当高并发下会导致大量的请求冲突,冲突导致大部分操作无功而返而浪费资源。

大表优化:

某个表有近千万数据,查询比较慢,如何优化?

当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下:

限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内;

读写分离:经典的数据库拆分方案,主库负责写,从库负责读;

通过分库分表的方式进行优化,主要有垂直拆分和水平拆分。

Nginx:

Nginx的主要场景包括HTTP服务器、静态服务器、反向代理、负载均衡等。

特点:热部署、可以高并发连接、低的内存消耗、处理响应请求很快、具有很高的可靠性。

c10k:epoll

nginx的特点:

更快:单次请求响应更快,高并发可以更快的处理响应

高扩展性:设计极具扩展性,由多个不同功能,不同层次、不同类型且耦合度极低的模块组成

高可靠性:很多高流量网站都在核心服务器上大规模使用Nginx

低内存消耗:一般1万个非活跃的Http Keep-Alive连接再Nginx中仅消耗2.5MB内存

高并发:单机支持10万以上的并发连接

热部署:master管理进程与worker工作进程的分离设计,使得Nginx能够支持热部署

开源协议:使用BSD许可协议,免费使用,且可修改源码。

Nginx为什么快:

nginx在启动后,会有一个master进程和多个worker进程。master用于管理worker进程,包含接收来自外界的信号,向各worker进程发送信号;所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的哪个进程注册listenfd读事件。一个完整的请求完全由worker进程来处理,节省锁带来的开销,每个worker进程都是独立的进程。

采用IO多路复用模型epoll,这个处理的worker不会阻塞,发送完请求后,空闲下来等待事件发生。

负载均衡算法:

现有的负载均衡算法主要分为静态和动态两类。静态负载均衡算法以固定的概率分配任务,不考虑服务器的状态信息,如轮转算法、加权轮转算法等;动态负载均衡算法以服务器的实时状态信息来决定任务的分配,如最小连接法、加权最小连接法等。

轮询法:将用户的请求轮流分配给服务器,就像是挨个数数,轮流分配。

随机法:随机选择一台服务器来分配任务。

最小连接法:将任何分配给此时具有最小连接数的节点。

Gin:

gin框架的好处:

快速:基于Radix树的路由,性能非常强大。

支持中间件:内置许多中间件,如Logger,Gzip,Authorization等。

崩溃恢复:可以捕捉panic引发的程序崩溃,使Web服务可以一直运行。

JSON验证:可以验证请求中JSON数据格式。

多种数据渲染方式:支持HTML、JSON、YAML、XML等数据格式的响应。

扩展性:非常简单扩展中间件。

gorm是如何实现的:

原生sql查询数据,reflect映射数据结构。

gin的handler底层数据结构:

handler是请求的处理对象,需实现ServeHTTP方法,ServeHTTP执行的是业务逻辑,一般定义的func(w http.ResponseWriter, r *http.Request)的方法需要经过http.HandlerFunc包装为Handler对象。

Handler函数是具有func(w http.ResponseWriter, r *http.Requests)签名的函数,需要经过HandlerFunc函数包装,否则不能作为路由的Handler对象,HandlerFunc实现了ServeHTTP接口方法的函数。

GRPC相对于RESTFUL有什么优势:

RPC框架实际是提供了一套机制,使得应用程序之间可进行通信,且遵从server/client模型,客户端使用RPC调用server端提供的接口时就像是调用本地的函数一样。

GRPC和Resultful API都提供一套通信机制,用于server、client模型通信,且都使用HTTP作为底层传输协议,严格来讲,GRPC使用的HTTP2.0,而Resultful API规则不一定),GRPC特有的优势如下:

1、GRPC可以通过Protobuf来定义接口,可以更加严格的接口约束条件,支持多种语言;

2、Protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高传输效率;

3、GRPC可以支持streaming流式通信(HTTP2.0),提高传输速度。

Django:

Django、Flask、Tornado对比:

(1)Django走的是大而全的方向,开发效率高。它与MTV框架,自带的ORM,admin后台管理,自带的sqlite数据库和开发测试用的服务器给开发者提高了超高的开发效率。

(2)Flask是轻量级的框架,自由、灵活、可扩展性很强,核心基于Werkzeug WSGI工具和jinjia2模板引擎。

(3)Torrnado走的是少而精的方向,性能优越。它最出名的是异步非柱塞的设计方式

Tornado的两大核心模块:

1、iostream: 对非阻塞式的socket进行简单的封装

2、ioloop:对I/O对路复用的封装,它实现了一个实例

什么是wsgi、uwsgi、uWSGI:

WSGI:用在python web框架(Django/Flask)编写的应用程序与web服务器之间的规范。

uWSGI:是一个web服务器,它实现了WSGI/uwsgi/HTTP等协议,用于接收Nginx转发的动态请求,处理后发个python应用程序。

uwsgi:是uWSGI服务器实现的独有协议,用于Nginx服务与uWSGI服务的通信规范

列举django中间的5个方法,以及django中间件的应用场景:

process_request: 请求进来时,权限认证

process_view: 路由匹配之后,能够得到视图函数

process_exception:异常时执行

process_template_response:模板渲染时执行

process_response:请求有响应时执行

Django请求生命周期:

1、uWSGI服务器通过wsgi协议,将HttpRequest交给web框架(Flask、Django)

2、首先到达request中间件,对请求对象进行校验或添加数据,例如:csrf、request.session,如果验证不通过直接跳转到response中间件

3、过URL配置文件找到urls.py文件

4、根据浏览器发送的URL,通过视图中间件去匹配不同的视图函数或视图类,如果没有找到相对的视图函数,就直接跳转到response中间件

5、在视图函数或视图类中进行进行业务逻辑处理,处理完返回到response中间件

6、模型类通过ORM获取数据库数据,并返回序列化json或渲染好的Template到response中间件

7、所有最后离开的响应都会达到response中间件,对响应的数据进行处理,返回HttpResponse给wsgi

8、wsgi通过uWSGI服务器,将响应的内容发送给浏览器

什么是CSRF,请描述其攻击原理,在Django中如何解决:

CSRF(cross-site request forgery)简称跨站请求伪造。

在post请求时,form表单或ajax里添加csrf_token,服务端开启CSRF中间件进行验证。

解决原理是页面添加csrf_token值后,用户通过URL访问(GET请求)该页面时,Django会在响应中自动帮我们生成cookie信息,返回给浏览器,同时在前端代码生成一个csrf_token值。

然后当你POST提交信息时,Django会自动比对cookie里和前端form表单或ajax提交上来的csrf_token值,两者一致,说明是当前浏览器发起的正常请求并处理业务逻辑返回响应。

那么第三方网站拿到你的cookie值为什么不能通过验证呢?

因为他没你前端的哪个随机生成的token值,他总不能跑你电脑面前查看你的浏览器前端页面自动随机生成的token值把。

1、django第一次响应来自某个客户端的请求时,服务器随机生成1个token值,把这个token保存在session中;同时服务器把这个token放到cookie中交给前端页面;

2、该客户端再次发起请求时,把这个token值加入到请求数据或者头信息中,一起传给服务器;

3、服务器校验前端请求带过来的token和session里的token是否一致。

请简述Django下的(内建)缓存机制:

Django根据设置的缓存方式,浏览器第一次请求时,cache会缓存单个变量或整个网页等内容到硬盘或者内存中,同时设置response头部。

当浏览器再次发起请求时,附带f-Modified-Since请求时间到Django。

Django发现f-Modified-Since会先去参数之后,会与缓存中的过期时间相比较,如果缓存时间比较新,则会重新请求数据,并缓存起来然后返回response给客户端。

如果缓存没有过期,则直接从缓存中提取数据,返回response给客户端。

查询集返回的列表过滤器有哪些:

all():返回所有数据

filter():返回满足条件的数据

exclude():返回满足条件之外的数据,相当于sql语句中where部分的not关键字

order_by():排序

路由层:

urlpatterns中的name与namespace的区别:name:给路由起一个别名;namespace:防止多个应用之间的路由重复。

include用作路由转发,通常,我们会在每个app里,各自创建一个urls.py路由模块,然后从根路由出发,将app所属的url请求,全部转发到相应的urls.py模块中。

视图层:

常用视图响应的方式有4种方式redirect、Response、HttpResponse和JsonResponse。

Celery:

异步任务:当用户在网站进行某个操作需要很长时间完成时,我们可以将这种操作交给Celery执行,直接返回给用户,等到Celery执行完成以后通知用户,大大提高网站的并发以及用户的体验感。

定时任务:向定时清楚沉余数据或批量在几百台机器执行某些命令或者任务,此时Celery可以轻松搞定。

Flask:

简述flask上下文管理流程:

请求到来时,将session和request封装到ctx对象种;2.对session做补充;3、将包含了request和session的ctx对象放到一个容器中(每个请求都会根据线程、协程加一个唯一标识);4.视图函数使用的时候需要根据当前线程或协程的唯一标识,获取ctx对象,再取ctx对象中取request和session(视图函数使用的时候,需要根据当前线程获取数据。)5.请求结束时,根据当前线程/协程的唯一表示,将这个容器上的数据移除。

Flask中多app应用时怎么完成:

使用Flask泪创建不同的app对象,然后借助DispatcherMiddleware类来实现。

Flask框架默认session的处理机制:

当请求刚进来时,Flask读取cookie中session对应的值,将该值解密并反序列化为字典,放入内存以便视图函数使用。当请求结束时,Flask会读取字典的值,进行序列化加密,写入到用户的cookie中。

Flask和Django最大的区别:

对于request,flask时导入进来的,而django时参数传递的;对于session,flask也导入进来的,而django时依附request对象传递进来的。

eventlet:

eventlet具有WSGI支持的异步框架

eventlet是python库函数,一个处理和网络相关的,另一个可以通过协程实现并发。

Tornado:

tornado是基于epoll的事件驱动框架,在网络事件上是无阻塞的。

DEVOPS:

CI/CD:

gitflow和gitlab flow:

gitflow通常包含master,develop,feature,hotfix、realease五个分支;gitlab flow增加了对预生产环境和生产环境的管理,即Master分支对应为开发环境的分支,预生产和生产环境由其他分支(如Pre-Production、Production)进行管理。

灰度发布:

在一般情况下,升级服务器端应用,需要将应用源码或程序包上传到服务器,然后停止掉老版本服务,再启动新版本。但是这种简单的发布方式存在两个问题,一方面,再新版本升级过程中,服务是暂时中断的,另一方面,如果新版本有BUG,升级失败,回滚起来非常麻烦,容易造成更长时间的服务不可用。

蓝绿部署:所谓蓝绿部署,是指同时运行两个版本的应用,蓝绿部署的时候,并不停止掉老版本,而是直接部署一套新版本,等新版本运行起来,再将流量切换到新版本上。但是蓝绿部署要求在升级过程中,同时运行两套程序,对硬件要求就是日常所需的二倍,比如日常运行时,需要10台服务器支撑业务,那么使用蓝绿部署,你就需要购置二十台服务器。

滚动发布:滚动发布能够解决掉蓝绿部署时对硬件要求倍增的问题。所谓滚动升级,就是在升级过程中,并不一下子启动所有新版本,是先启动一台新版本,再停止一台老版本,然后再启动一台新版本,再停止一台老版本,直接升级完成,这样的话,如果日常需要10台服务器,那么升级过程中也就只需要11台就行。但是滚动升级有一个问题,在开始滚动升级后,流量会直接流向已经启动起来的新版本,但是这个时候,新版本是不一定可用的,比如需要进一步的测试才能确认。那么滚动升级期间,整个系统就处于非常不稳定的状态,如果发现了问题,也比较难以确定是新版本还是老版本造成的问题。为了解决这个问题,我们需要为滚动升级实现流量控制能力。

金丝雀:在灰度发布开始后,先启动一个新版本应用,但是并不直接将流量切过来,而是测试人员对新版本进行线上测试,启动的这个新版本应用,就是我们的金丝雀。如果没有问题,那么可以将少量的用户流量导入到新版本上,然后对新版本做运行状态观察,收集各种运行时数据,如果此时对新旧版本做各种数据对比,就是所谓的A/B测试。当确认新版本运行良好后,再逐步将更多的流量导入到新版本上,在此期间,还可以不断地调整新旧两个版本地运行地服务器副本数量,以使得新版本能够承受越来越大的流量压力。直接将100%的流量都切换到新版本上,最后关闭剩下的老版本服务,完成灰度发布。

Jenkins Pipeline:

pipeline,简单来说,就是一套运行在jenkins上的工作流框架。将原来独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排和可视化的工作。

pipeline脚本由Groovy语言实现。

pipeline支持两种语法:Declarative(声明式脚本)| Scripted Pipeline(脚本式语法)

pipeline {
  agent any //环境maven、jdk等
  
  stages {   // 项目构建
    stage('pull code') { //拉取代码
       steps {  //具体实施步骤
          echo 'pull code'
       }
    }
    stage('build project') { // 编译打包
      steps {      //具体实施步骤
        echo 'build project' // 打包命令
      }
    }
    stage('publish project') { // 部署上线
      steps {      //具体实施步骤
        echo 'publish project'  //部署命令
      }
    }
  }
}

监控:

zabbix有哪些组件:

1)Zabbix Server:负责接收agent发送的报告信息的核心组件,所有配置、统计数据及操作数据均操作数据均由其组织进行

2)Database Storage:专用于存储所有配置信息,以及有zabbix收集的数据

3)Web interface(frontend):zabbix的GUI接口,通常与server运行在同一台机器上

4)Proxy:可选组件,常用于分布式监控环境中,代理Server收集部分被监控数据并统一发往Server端

5)Agent:部署在被监控主机上,负责收集本地数据并发送Server端或者Proxy端

一个监控系统的运行流程:

zabbix agent需要安装到被监控的主机上,它负责定期收集各项数据,并发送到zabbix server端,zabbix server将数据存储到数据库中,zabbix web根据数据在前端进行展示和绘图。

Flink的核心概念:

Flink的核心概念主要有四个:Event Streams、State、Time和Snapshots。

Event Time:事件创建的时间

Ingestion Time:数据进入Flink的时间

Processing Time:执行操作算子的本地系统时间,与机器相关

Flink窗口:

窗口(window)就是将无限流切割为有限流的一种方式,它会将流数据粉发到有限大小的桶(bucket)中进行分析;

滚动窗口:将数据依据固定的长度对数据进行切分,时间对齐,窗口长度固定,没有重叠。

滑动窗口:将数据依据固定的窗口长度对数据进行切分,滑动窗口长度有重叠。

会话窗口:由一系列事件组合-个指定时间长度的timeout间隙组成,也就是一段时间没有接收到新数据就会产生新的窗口,时间无对齐

水位线(watermark):

水位线是一种衡量Event Time进展的机制,用来处理实时数据的乱序问题的,通常是水位线和窗口结合使用实现的。包括周期性分配水位线、定点水位线。

日志:

filebeat源码分析-启动:

filebeat的工作原理。简单来说:对于配置文件(filebeat.yml)中prospectors配置的每个日志文件。简单来说:对于配置文件中prospectors配置的每个日志文件,Filebeat启动harvester。每个harvester读取一个日志文件的内容,日志数据发送给soopler(后台处理程序),spooler汇集的事件聚合数据发送到配置的输出目标。

filebeat的main.go

package main
import (
    "os"
    "github.com/elastic/beats/filebeat/cmd"
)
func main() {
    if err := cmd.RootCmd.Execute(); err != nil {
        os.Exit(1)
    }
}

进入到filebeat/cmd执行 

package cmd

import (
    "flag"

    "github.com/spf13/pflag"

    "github.com/elastic/beats/filebeat/beater"

    cmd "github.com/elastic/beats/libbeat/cmd"
)

// Name of this beat
var Name = "filebeat"

// RootCmd to handle beats cli
var RootCmd *cmd.BeatsRootCmd

func init() {
    var runFlags = pflag.NewFlagSet(Name, pflag.ExitOnError)
    runFlags.AddGoFlag(flag.CommandLine.Lookup("once"))
    runFlags.AddGoFlag(flag.CommandLine.Lookup("modules"))

    RootCmd = cmd.GenRootCmdWithRunFlags(Name, "", beater.New, runFlags)
    RootCmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("M"))
    RootCmd.TestCmd.Flags().AddGoFlag(flag.CommandLine.Lookup("modules"))
    RootCmd.SetupCmd.Flags().AddGoFlag(flag.CommandLine.Lookup("modules"))
    RootCmd.AddCommand(cmd.GenModulesCmd(Name, "", buildModulesManager))
}

 RootCmd在这一句初始化RootCmd = cmd.GenRootCmdWithRunFlags(Name, "", beater.New, runFlags)

beater.New跟进去看到是filebeat.go func New(b beat.Beat, rawConfig common.Config) (beat.Beater, error) {...}

现在进入GenRootCmdWithRunFlags方法,一路跟进去到GenRootCmdWithSettings,真正的初始化是在这个方法里面。

忽略前面的一段初始化值方法,看到RunCmd的初始化在:

rootCmd.RunCmd = genRunCmd(settings, beatCreator, runFlags)

进入getRunCmd,看到执行代码:

err := instance.Run(settings, beatCreator)

跟到\elastic\beats\libbeat\cmd\instance\beat.go的Run方法

b, err := NewBeat(name, idxPrefix, version)

这里新建了beat

在方法末尾

return b.launch(settings, bt)

 在launch的末尾

return beater.Run(&b.Beat)

beat开始启动

因为启动的是filebeat,我们到filebeat.go的Run方法

func (fb *Filebeat) Run(b *beat.Beat) error {
       var err error
       config := fb.config

       if !fb.moduleRegistry.Empty() {
              err = fb.loadModulesPipelines(b)
              if err != nil {
                     return err
              }
       }

       waitFinished := newSignalWait()
       waitEvents := newSignalWait()

       // count active events for waiting on shutdown
       wgEvents := &eventCounter{
              count: monitoring.NewInt(nil, "filebeat.events.active"),
              added: monitoring.NewUint(nil, "filebeat.events.added"),
              done:  monitoring.NewUint(nil, "filebeat.events.done"),
       }
       finishedLogger := newFinishedLogger(wgEvents)

       // Setup registrar to persist state
       registrar, err := registrar.New(config.RegistryFile, config.RegistryFilePermissions, config.RegistryFlush, finishedLogger)
       if err != nil {
              logp.Err("Could not init registrar: %v", err)
              return err
       }

       // Make sure all events that were published in
       registrarChannel := newRegistrarLogger(registrar)

       err = b.Publisher.SetACKHandler(beat.PipelineACKHandler{
              ACKEvents: newEventACKer(finishedLogger, registrarChannel).ackEvents,
       })
       if err != nil {
              logp.Err("Failed to install the registry with the publisher pipeline: %v", err)
              return err
       }

       outDone := make(chan struct{}) // outDone closes down all active pipeline connections
       crawler, err := crawler.New(
              channel.NewOutletFactory(outDone, wgEvents).Create,
              config.Inputs,
              b.Info.Version,
              fb.done,
              *once)
       if err != nil {
              logp.Err("Could not init crawler: %v", err)
              return err
       }

       // The order of starting and stopping is important. Stopping is inverted to the starting order.
       // The current order is: registrar, publisher, spooler, crawler
       // That means, crawler is stopped first.

       // Start the registrar
       err = registrar.Start()
       if err != nil {
              return fmt.Errorf("Could not start registrar: %v", err)
       }

       // Stopping registrar will write last state
       defer registrar.Stop()

       // Stopping publisher (might potentially drop items)
       defer func() {
              // Closes first the registrar logger to make sure not more events arrive at the registrar
              // registrarChannel must be closed first to potentially unblock (pretty unlikely) the publisher
              registrarChannel.Close()
              close(outDone) // finally close all active connections to publisher pipeline
       }()

       // Wait for all events to be processed or timeout
       defer waitEvents.Wait()

       // Create a ES connection factory for dynamic modules pipeline loading
       var pipelineLoaderFactory fileset.PipelineLoaderFactory
       if b.Config.Output.Name() == "elasticsearch" {
              pipelineLoaderFactory = newPipelineLoaderFactory(b.Config.Output.Config())
       } else {
              logp.Warn(pipelinesWarning)
       }

       if config.OverwritePipelines {
              logp.Debug("modules", "Existing Ingest pipelines will be updated")
       }

       err = crawler.Start(b.Publisher, registrar, config.ConfigInput, config.ConfigModules, pipelineLoaderFactory, config.OverwritePipelines)
       if err != nil {
              crawler.Stop()
              return err
       }

       // If run once, add crawler completion check as alternative to done signal
       if *once {
              runOnce := func() {
                     logp.Info("Running filebeat once. Waiting for completion ...")
                     crawler.WaitForCompletion()
                     logp.Info("All data collection completed. Shutting down.")
              }
              waitFinished.Add(runOnce)
       }

       // Register reloadable list of inputs and modules
       inputs := cfgfile.NewRunnerList(management.DebugK, crawler.InputsFactory, b.Publisher)
       reload.Register.MustRegisterList("filebeat.inputs", inputs)

       modules := cfgfile.NewRunnerList(management.DebugK, crawler.ModulesFactory, b.Publisher)
       reload.Register.MustRegisterList("filebeat.modules", modules)

       var adiscover *autodiscover.Autodiscover
       if fb.config.Autodiscover != nil {
              adapter := fbautodiscover.NewAutodiscoverAdapter(crawler.InputsFactory, crawler.ModulesFactory)
              adiscover, err = autodiscover.NewAutodiscover("filebeat", b.Publisher, adapter, config.Autodiscover)
              if err != nil {
                     return err
              }
       }
       adiscover.Start()

       // Add done channel to wait for shutdown signal
       waitFinished.AddChan(fb.done)
       waitFinished.Wait()

       // Stop reloadable lists, autodiscover -> Stop crawler -> stop inputs -> stop harvesters
       // Note: waiting for crawlers to stop here in order to install wgEvents.Wait
       //       after all events have been enqueued for publishing. Otherwise wgEvents.Wait
       //       or publisher might panic due to concurrent updates.
       inputs.Stop()
       modules.Stop()
       adiscover.Stop()
       crawler.Stop()

       timeout := fb.config.ShutdownTimeout
       // Checks if on shutdown it should wait for all events to be published
       waitPublished := fb.config.ShutdownTimeout > 0 || *once
       if waitPublished {
              // Wait for registrar to finish writing registry
              waitEvents.Add(withLog(wgEvents.Wait,
                     "Continue shutdown: All enqueued events being published."))
              // Wait for either timeout or all events having been ACKed by outputs.
              if fb.config.ShutdownTimeout > 0 {
                     logp.Info("Shutdown output timer started. Waiting for max %v.", timeout)
                     waitEvents.Add(withLog(waitDuration(timeout),
                            "Continue shutdown: Time out waiting for events being published."))
              } else {
                     waitEvents.AddChan(fb.done)
              }
       }

       return nil
}

构造register和crawler,用于监控文件状态变更和数据采集,然后

err = crawler.Start(b.Publisher, registrar, config.ConfigInput, config.ConfigModules, pipelineLoaderFactory, config.OverwritePipelines)

crawler开始启动采集数据

for _, inputConfig := range c.inputConfigs {
       err := c.startInput(pipeline, inputConfig, r.GetStates())
       if err != nil {
              return err
       }
}

crawler的Start方法里面根据每个配置的输入调用一次startInput

func (c *Crawler) startInput(
       pipeline beat.Pipeline,
       config *common.Config,
       states []file.State,
) error {
       if !config.Enabled() {
              return nil
       }

       connector := channel.ConnectTo(pipeline, c.out)
       p, err := input.New(config, connector, c.beatDone, states, nil)
       if err != nil {
              return fmt.Errorf("Error in initing input: %s", err)
       }
       p.Once = c.once

       if _, ok := c.inputs[p.ID]; ok {
              return fmt.Errorf("Input with same ID already exists: %d", p.ID)
       }

       c.inputs[p.ID] = p

       p.Start()

       return nil
}

根据配置的input,构造log/input

func (p *Input) Run() {
       logp.Debug("input", "Start next scan")

       // TailFiles is like ignore_older = 1ns and only on startup
       if p.config.TailFiles {
              ignoreOlder := p.config.IgnoreOlder

              // Overwrite ignore_older for the first scan
              p.config.IgnoreOlder = 1
              defer func() {
                     // Reset ignore_older after first run
                     p.config.IgnoreOlder = ignoreOlder
                     // Disable tail_files after the first run
                     p.config.TailFiles = false
              }()
       }
       p.scan()

       // It is important that a first scan is run before cleanup to make sure all new states are read first
       if p.config.CleanInactive > 0 || p.config.CleanRemoved {
              beforeCount := p.states.Count()
              cleanedStates, pendingClean := p.states.Cleanup()
              logp.Debug("input", "input states cleaned up. Before: %d, After: %d, Pending: %d",
                     beforeCount, beforeCount-cleanedStates, pendingClean)
       }

       // Marking removed files to be cleaned up. Cleanup happens after next scan to make sure all states are updated first
       if p.config.CleanRemoved {
              for _, state := range p.states.GetStates() {
                     // os.Stat will return an error in case the file does not exist
                     stat, err := os.Stat(state.Source)
                     if err != nil {
                            if os.IsNotExist(err) {
                                   p.removeState(state)
                                   logp.Debug("input", "Remove state for file as file removed: %s", state.Source)
                            } else {
                                   logp.Err("input state for %s was not removed: %s", state.Source, err)
                            }
                     } else {
                            // Check if existing source on disk and state are the same. Remove if not the case.
                            newState := file.NewState(stat, state.Source, p.config.Type, p.meta)
                            if !newState.FileStateOS.IsSame(state.FileStateOS) {
                                   p.removeState(state)
                                   logp.Debug("input", "Remove state for file as file removed or renamed: %s", state.Source)
                            }
                     }
              }
       }
}

input开始根据配置的输入路径扫描所有符合的文件,并启动harvester

func (p *Input) scan() {
       var sortInfos []FileSortInfo
       var files []string

       paths := p.getFiles()

       var err error

       if p.config.ScanSort != "" {
              sortInfos, err = getSortedFiles(p.config.ScanOrder, p.config.ScanSort, getSortInfos(paths))
              if err != nil {
                     logp.Err("Failed to sort files during scan due to error %s", err)
              }
       }

       if sortInfos == nil {
              files = getKeys(paths)
       }

       for i := 0; i < len(paths); i++ {

              var path string
              var info os.FileInfo

              if sortInfos == nil {
                     path = files[i]
                     info = paths[path]
              } else {
                     path = sortInfos[i].path
                     info = sortInfos[i].info
              }

              select {
              case <-p.done:
                     logp.Info("Scan aborted because input stopped.")
                     return
              default:
              }

              newState, err := getFileState(path, info, p)
              if err != nil {
                     logp.Err("Skipping file %s due to error %s", path, err)
              }

              // Load last state
              lastState := p.states.FindPrevious(newState)

              // Ignores all files which fall under ignore_older
              if p.isIgnoreOlder(newState) {
                     err := p.handleIgnoreOlder(lastState, newState)
                     if err != nil {
                            logp.Err("Updating ignore_older state error: %s", err)
                     }
                     continue
              }

              // Decides if previous state exists
              if lastState.IsEmpty() {
                     logp.Debug("input", "Start harvester for new file: %s", newState.Source)
                     err := p.startHarvester(newState, 0)
                     if err == errHarvesterLimit {
                            logp.Debug("input", harvesterErrMsg, newState.Source, err)
                            continue
                     }
                     if err != nil {
                            logp.Err(harvesterErrMsg, newState.Source, err)
                     }
              } else {
                     p.harvestExistingFile(newState, lastState)
              }
       }
}

在harvester的Run看到一个死循环读取message,预处理之后交给由forwarder发送到目标输出

message, err := h.reader.Next()
h.sendEvent(data, forwarder)

至此,整个filebeat的启动到发送数据就处理完了。

filebeat源码解析-配置文件解析:

调用cfgfile.Load方法解析到cfg对象,进入load方法

func Load(path string, beatOverrides *common.Config) (*common.Config, error) {
       var config *common.Config
       var err error

       cfgpath := GetPathConfig()

       if path == "" {
              list := []string{}
              for _, cfg := range configfiles.List() {
                     if !filepath.IsAbs(cfg) {
                            list = append(list, filepath.Join(cfgpath, cfg))
                     } else {
                            list = append(list, cfg)
                     }
              }
              config, err = common.LoadFiles(list...)
       } else {
              if !filepath.IsAbs(path) {
                     path = filepath.Join(cfgpath, path)
              }
              config, err = common.LoadFile(path)
       }
       if err != nil {
              return nil, err
       }

       if beatOverrides != nil {
              config, err = common.MergeConfigs(
                     defaults,
                     beatOverrides,
                     config,
                     overwrites,
              )
              if err != nil {
                     return nil, err
              }
       } else {
              config, err = common.MergeConfigs(
                     defaults,
                     config,
                     overwrites,
              )
       }

       config.PrintDebugf("Complete configuration loaded:")
       return config, nil
}

如果不输入配置文件,使用configfiles定义文件

configfiles = common.StringArrFlag(nil, "c", "beat.yml", "Configuration file, relative to path.config")

如果输入配置文件进入else分支

config, err = common.LoadFile(path)

根据配置文件构造config对象在

c, err := yaml.NewConfigWithFile(path, configOpts...)

filebeat源码解析-采集文件扫描:

在log/input.Run()方法调用Input.scan()

进入scan方法看到

paths := p.getFiles()

getFiles方法即获得所有采集文件路径的方法

func (p *Input) getFiles() map[string]os.FileInfo {
       paths := map[string]os.FileInfo{}

       for _, path := range p.config.Paths {
              matches, err := filepath.Glob(path)
              if err != nil {
                     logp.Err("glob(%s) failed: %v", path, err)
                     continue
              }

       OUTER:
              // Check any matched files to see if we need to start a harvester
              for _, file := range matches {

                     // check if the file is in the exclude_files list
                     if p.isFileExcluded(file) {
                            logp.Debug("input", "Exclude file: %s", file)
                            continue
                     }

                     // Fetch Lstat File info to detected also symlinks
                     fileInfo, err := os.Lstat(file)
                     if err != nil {
                            logp.Debug("input", "lstat(%s) failed: %s", file, err)
                            continue
                     }

                     if fileInfo.IsDir() {
                            logp.Debug("input", "Skipping directory: %s", file)
                            continue
                     }

                     isSymlink := fileInfo.Mode()&os.ModeSymlink > 0
                     if isSymlink && !p.config.Symlinks {
                            logp.Debug("input", "File %s skipped as it is a symlink.", file)
                            continue
                     }

                     // Fetch Stat file info which fetches the inode. In case of a symlink, the original inode is fetched
                     fileInfo, err = os.Stat(file)
                     if err != nil {
                            logp.Debug("input", "stat(%s) failed: %s", file, err)
                            continue
                     }

                     // If symlink is enabled, it is checked that original is not part of same input
                     // It original is harvested by other input, states will potentially overwrite each other
                     if p.config.Symlinks {
                            for _, finfo := range paths {
                                   if os.SameFile(finfo, fileInfo) {
                                          logp.Info("Same file found as symlink and originap. Skipping file: %s", file)
                                          continue OUTER
                                   }
                            }
                     }

                     paths[file] = fileInfo
              }
       }

       return paths
}

根据配置的路径path使用

matches, err := filepath.Glob(path)

获取所有匹配到的文件全路径。

filebeat源码解析4-发送过程:

harvest.send方法发送了数据到output,继续跟进去,到err := forwarder.Send(data)

func (f *Forwarder) Send(data *util.Data) error {
       ok := f.Outlet.OnEvent(data)
       if !ok {
              logp.Info("Input outlet closed")
              return errors.New("input outlet closed")
       }

       return nil
}

调用Outlet.OnEvent发送data

点进去发现是一个接口

type Outlet interface {
       OnEvent(data *util.Data) bool
}

经过调试观察,elastic\beats\filebeat\channel\outlet.go实现了这个接口

func (o *outlet) OnEvent(d *util.Data) bool {
       if !o.isOpen.Load() {
              return false
       }

       event := d.GetEvent()
       if d.HasState() {
              event.Private = d.GetState()
       }

       if o.wg != nil {
              o.wg.Add(1)
       }

       o.client.Publish(event)
       return o.isOpen.Load()
}

通过client.Publish发送数据,client也是一个接口

type Client interface {
       Publish(Event)
       PublishAll([]Event)
       Close() error
}

调式之后,client使用的是elastic\beats\libbeat\publisher\pipeline\client.go的client对象,publish方法即发送日志的方法,如果需要在发送前改造日志格式,可在这里添加代码,如下面的解析日志代码。

func (c *client) publish(e beat.Event) {
       var (
              event   = &e
              publish = true
              log     = c.pipeline.logger
       )

       c.onNewEvent()

       if !c.isOpen.Load() {
              // client is closing down -> report event as dropped and return
              c.onDroppedOnPublish(e)
              return
       }

       if c.processors != nil {
              var err error

              event, err = c.processors.Run(event)
              publish = event != nil
              if err != nil {
                     // TODO: introduce dead-letter queue?

                     log.Errorf("Failed to publish event: %v", err)
              }
       }

       if event != nil {
              e = *event
       }

       open := c.acker.addEvent(e, publish)
       if !open {
              // client is closing down -> report event as dropped and return
              c.onDroppedOnPublish(e)
              return
       }

       if !publish {
              c.onFilteredOut(e)
              return
       }

       //解析日志
       error:=ParselogMsg(event)
       if error!=nil{
              log.Errorf("###出现错误")
       }
       //

       e = *event
       pubEvent := publisher.Event{
              Content: e,
              Flags:   c.eventFlags,
       }

       if c.reportEvents {
              c.pipeline.waitCloser.inc()
       }

       var published bool
       if c.canDrop {
              published = c.producer.TryPublish(pubEvent)
       } else {
              published = c.producer.Publish(pubEvent)
       }

       if published {
              c.onPublished()
       } else {
              c.onDroppedOnPublish(e)
              if c.reportEvents {
                     c.pipeline.waitCloser.dec(1)
              }
       }
}

kibana API:

鉴权的时候用过限制查询objects,或者创建objects:

Find
Get /api/saved_objects/_find
Get /s//api/saved_objects/_find

Create
POST /api/saved_objects/
POST /api/saved_objects//

ES中的倒排索引是什么:

倒排索引,是通过分词策略,形成了词和文章的映射关系表,也称倒排表,这种词典+映射表即为倒排索引。

其中词典中存储词元,倒排表中存储该词元在哪些文中出现的位置。有了倒排索引,就能实现O(1)时间复杂度的效率检索文章了,极大的提高了检索效率。

倒排索引的底层基于FST(Finite State Transducer)数据结构。

FST有两大优势:

1)占用空间小,通过对词典中单词前缀和后缀的重复利用,压缩了存储空间。

2)查询速度快,O(len(str))的查询时间复杂度。

ES是如何实现master选举的:

前置条件:

1)只有是候选主节点(master:true)的节点才能成为主节点。

2)最小主节点(min_master_nodes)的目的是防止脑裂。

Elasticsearch的选主是ZenDiscovery模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此)和Unicast(单播模块包含一个主机列表以控制哪些节点需要ping通)这两部分;

获取主节点的核心入口为findMaster,选择主节点成功返回对应Master,否则返回null。

第一步:确认候选主节点数达标,elasticsearch.yml 设置的值discovery.zen.minimum_master_nodes;

第二步:对所有候选主节点根据nodeId字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个节点,暂且认为它是master节点。

第三步:如果对某个节点的投票数达到一定的值并且该节点自己也选举自己,那这个节点就是master,否则重新选举一直到满足上述条件。

详细描述以下ES索引文档的过程:

这里的索引文件应该理解为文档写入es,创建索引的过程。

第一步:客户端向集群某节点写入数据,发送请求。

第二步:协调节点接受到请求后,默认使用文档ID参与计算(也支持通过routing),得到该文档属于哪个分片。随后请求会被转到另外的节点。

第三步:当分片所在的节点接收到来自协调节点的请求后,会将请求写入到Memory Buffer,然后定时(默认是每隔1秒)写入到Filesystem Cache,这个从Momery Buffer到Filesystem Cache的过程就叫做refresh;

第四步:当然在某些情况下,存在Memory Buffer和Filesystem Cache的数据可能会丢失,ES是通过translog的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到translog中,当Filesystem cache中的数据写入到磁盘中时,才会清除掉,这个过程叫做flush;

第五步:在flush过程中,内存中的缓冲将被清除,内容被写入一个新段,段的fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的translog将被删除并开始一个一个新的translog。

第六步:flush触发的时机是定时触发(默认30分钟)或者translog变得太大(默认为512M)时。

详细描述一下ES更新和删除文档的过程:

删除和更新也都是写操作,但是Elasticsearch中的文档是不可变的,因此不能被删除或者改动以展示其变更。

磁盘上的每个段都有一个相应的.del文件。当删除请求发送后,文档并没有真的被删除,而是在.del文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤。当段合并时,在.del文件中被标记为删除的文档将不会被写入新段。

在新的文档被创建时,Elasticsearch会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤。

详细描述一下ES搜索的过程:

搜索被执行成一个两阶段过程,即Query Then Fetch:

Query阶段:

查询会广播到索引中每一个分片拷贝(主分片或者副本分片),每个分片在本地执行搜索并构建一个匹配文档的大小为from + size的优先队列。PS:在搜索的时候是会查询Filesystem Cache的,但是有部分还在Memory Buffer,所以搜索是近实时的。

每个分片返回各自优先队列中所有文档的ID和排序值给协调点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。

Fetch阶段:

协调节点辨认出哪些文档需要被取回并向相关的分片提交多个GET请求,每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点。一旦所有的文档被取回了,协调节点返回结果给客户端。

索引是什么:

ES集群包含多个索引,每个索引包含一种表,表包含多个文档,并且每个文档包含不同的属性。

请解释什么是分片:

随着索引文件的增加,磁盘容量、处理能力都会变得不够,在这种情况下,将索引数据切分成小段,这就叫分片(SHARDS)。他的出现大大改进了查询的效率。

什么是副本,他的作用是什么:

副本是分片的完整拷贝,副本的作用是增加了查询的吞吐率和在极端负载情况下获得高可用的能力。副本有效的帮助处理用户请求。

在ES集群中增加和创建索引的步骤是什么:

可以在kibana中配置新的索引,进行Fields Mapping,设置索引别名。也可以通过HTTP请求来创建索引。

ES支持哪些类型的查询:

主要分为匹配(文本)查询和基于Term查询。

文本查询包括基本匹配,match phrase, multi-match, match phrase prefix, common terms, query-string, simple query string.

Term查询,比如term exists, type, term set, range, prefix, ids, wildcard, regexp, and fuzzy.

Elasticsearch在部署时,对Linux的设置有哪些优化方法:

1)关闭缓存sway;

2)堆内存设置为:Min(节点内存/2)

3) 设置最大文件句柄数

4)线程池+队列大小根据业务需要做调整

5)磁盘存储raid方式——存储有条件使用RAID10,增加但节点性能以及避免单节点故障。

ElasticSearch中的分析器是什么:

在ES中索引数据时,数据由为索引定义的Analyzer在内部进行转换,分析器有一个Tokenizer和零个或多个TokenFilter组成。编译器可以在一个或多个CharFilter之前。分析模块允许你在逻辑名称下注册分析器,然后可以在映射定义或某些API中引用它们。

ElasticSearch附带了许多可以随时使用的预建分析器。或者,您可以组合内置的字符过滤器,编译器来创建自定义分析器。

在并发情况下,ES如何保证读写一致:

可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突;

另外对于写操作,一致性级别支持guorum/one/all,默认为quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点重建。

对于读操作,可以设置replication为sync(默认),这使得操作在主分片和副分片都完成后才返回;如果设置replication为async时,也可以通过设置搜索请求参数_prefrence为primary来查询主分片,确保文档是最新版本。

ES对于大数据量(上亿量级)的聚合如何实现:

ElasticSearch提供的首个近似聚合是cardinality度量,它提供一个字段的基数,即该字段的distinct或者unique值得数目。它是基于HLL算法得。HLL会先对我们得输入作哈希运算,然后根据哈希运算得结果中得bits做概率估算从而得到基数。其特点是:可配置得精度,用来控制内存得使用(更精确=更多内存);小的数据集精度是非常高得;我们可以通过配置参数,来设置去重需要得固定内存使用量,无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。

对于GC方面,在使用ES时要注意什么:

1)倒排词典的索引需要常驻内存,无法GC,需要监控data node上segment memory增长趋势;

2)各类缓存,field cache,filter cache, indexing cache, bulk queue等等,需设置合理的大小,并且要应该根据最坏的情况来看heap是否够用,也就是各类缓存全部占满的时候,还有heap空间可以分配给其他任务吗?避免采用clear chache等方式来释放内存

3)避免返回大量结果集的搜索与聚合。确实需要大量拉取数据的场景,可以采用scan & scroll api来实现。

4)cluster stats驻留内存无法水平扩展,超大规模集群可以考虑分拆成多个集群通过tribe node连接。

5)想知道heap够不够,必须结合实际应用场景,并对集群的heap使用情况做持续的监控。

ES调用实践:

一、Linux参数调优:

1、关闭交换分区,防止内存置换降低性能。将/etc/fstab文件中包含swap的行注释掉。

2、磁盘挂载选项:

noatime:禁止记录访问时间戳,提高文件系统读写性能

data=writeback:不记录data journal,提高文件系统写入性能

barrier=0:barrier保证journal先于data刷到磁盘,上面关闭了journal,这里的barrier也就没必要开启了。

nobh:关闭buffer_head,防止内核打断大块数据的IO操作。

3、对于SSD磁盘,采用电梯调度算法,因为SSD提供了更智能的请求调度算法,不需要内核去做多余的调整。

二、ES节点配置

1、适当增大写入buffer和bulk队列长度,提高写入性能和稳定性

indices.memory.index_buffer_size:15%
thread_pool.bulk.queue_size:1024

2、计算disk使用量时,不考虑正在搬迁的shard

在规模比较大的集群中,可以防止新建shard时扫描所有shard的元数据,提升shard分配速度

cluster.routing.allocation.disk.include_relocations: false

 三、ES使用方式

1、控制字段的存储选项:ES底层使用Lucene存储数据,主要包括行存(StoreFiled)、列存(DocValues)和倒排索引(InvertIndex)三部分。

2、开启最佳压缩

3、bulk批量写入

4、调整translog同步策略:对于每个写入请求都做一个flush,刷新translog数据到磁盘上。如果可以接受一定概率的数据丢失,可以通过命令调整translog持久化策略为异步周期性执行。

5、调整refresh_interval:es必须通过refresh的过程把内存中的数据转换成Lucene的完整segment后,才可以搜索。

6、merge并发控制:小segment合并成大segment。

7、写入数据不指定_id,让ES自动产生

8、使用routing:routing相同的写入同一个分片

9、为string类型的字段选合适的存储方式

10、查询时,使用query-bool-filter组合取代普通query

11、index按日期滚动,便于管理

12、按需控制index的分片数和副本数:一个节点上该index的shard数量,让shard尽量分配到不同节点上。

稳定性调优:

一、Linux参数调优:

# 修改系统资源限制
# 单用户可以打开的最大文件数量,可以设置为官方推荐的65536或更大些
echo "* - nofile 655360" >>/etc/security/limits.conf
# 单用户内存地址空间
echo "* - as unlimited" >>/etc/security/limits.conf
# 单用户线程数
echo "* - nproc 2056474" >>/etc/security/limits.conf
# 单用户文件大小
echo "* - fsize unlimited" >>/etc/security/limits.conf
# 单用户锁定内存
echo "* - memlock unlimited" >>/etc/security/limits.conf

# 单进程可以使用的最大map内存区域数量
echo "vm.max_map_count = 655300" >>/etc/sysctl.conf

# TCP全连接队列参数设置, 这样设置的目的是防止节点数较多(比如超过100)的ES集群中,节点异常重启时全连接队列在启动瞬间打满,造成节点hang住,整个集群响应迟滞的情况
echo "net.ipv4.tcp_abort_on_overflow = 1" >>/etc/sysctl.conf
echo "net.core.somaxconn = 2048" >>/etc/sysctl.conf

# 降低tcp alive time,防止无效链接占用链接数
echo 300 >/proc/sys/net/ipv4/tcp_keepalive_time

二、ES节点配置:

1、jvm.options:

-Xms和-Xmx设置为相同的值,推荐设置为机器内存的一半左右,剩余一半留给系统cache使用。

  • jvm内存建议不要低于2G,否则有可能因为内存不足导致ES无法正常启动或OOM
  • jvm建议不要超过32G,否则jvm会禁用内存对象指针压缩技术,造成内存浪费

2、elasticsearch.yml:

  • 设置内存熔断参数,防止写入或查询压力过高导致OOM,具体数值可根据使用场景调整。 indices.breaker.total.limit: 30% indices.breaker.request.limit: 6% indices.breaker.fielddata.limit: 3%
  • 调小查询使用的cache,避免cache占用过多的jvm内存,具体数值可根据使用场景调整。 indices.queries.cache.count: 500 indices.queries.cache.size: 5%
  • 单机多节点时,主从shard分配以ip为依据,分配到不同的机器上,避免单机挂掉导致数据丢失。 cluster.routing.allocation.awareness.attributes: ip node.attr.ip: 1.1.1.1

三、es使用方式:

1、节点数较多,增加转有master节点

2、控制index,shard数量

3、segment Memory优化

容器技术:

namespace设置的意图是什么:

独立的协议栈被隔离到不同的命名空间中,处于不同命名空间中的网络栈是完全隔离的,彼此无法通信。

每个namespace需要包含什么:

进程,嵌套字,网络设备等。

如何在Linux网络协议栈的基础上支持这些私有的独立协议栈:

实现的核心:让Linux网络的全局变量成为一个namespace变量的成员,然后为协议栈的函数调用加入一个Namespace参数。所有网络设备都只属于一个命名空间(物理设备关联到root,虚拟设备创建并关联到给定的ns,但可以在不同的网络ns之间转移设备)。

不同的ns网络如何互相通信:

使用Veth设备时,利用成对的Veth设备对能直接将两个网络ns连接起来。

# 创建Veth设备
ip link add veth0 type veth peer name veth1
# 查看Veth设备对信息
ip link show
# 将veth1转移到netns1中
ip link set veth1 netns netns1
# 分配ip地址
ip netns exec netns1 ip addr add 10.1.1.1/24 dev veth1
# 启动
ip netns exec netns1 ip link set dev veth1 up

怎么查看对端的veth设备:

使用ethtool工具。

#Stepq: 查询对端接口在设备列表中的序列号
ip netns exec netns1 ethtool -S veth1
#Step2: 通过grep查看特定序列号代表的设备
ip netns exec netns2 ip link | grep 5

docker支持几种网络模式:

四种。host, container, none; bridge(默认)

docker bridge模式如何支持网络的:

1、Docker Deamon启动时会创建一个虚拟网桥(docker0),并给网桥分配一个子网。

2、每个docker容器,都创建一个虚拟以太网络设备(Veth设备对),一端连接docker0,一端连接使用namespace容器内的eth0设备,然后从网桥地址给eth0接口分配一个ip地址。

docker网络模型的局限是什么:

Docker没有考虑多主机互联的网络方案。在同一台机器内的容器之间可以互相通信,不同主机间的容器不能互相通信,甚至有可能因为在不同主机上的docker0地址段相同而导致不同主机的容器在相同的地址范围内,可以协调好端口分配或使用动态端口分配技术来解决这个问题。

Docker是Linux容器的封装,将应用和以来包到一个可移植的容器中,然后发布到Linux机器上。容器完全使用沙箱机制,相互之间不会由任何接口。

Docker本质上是运行在宿主机的进程,它通过namespace实现了资源隔离,并通过cgroups实现了资源限制,同时通过写时复制(copy-on-write)实现了高效的文件操作。

Namespace:

Linux内核中提供了6种namespace隔离的系统调用,分别完成对文件系统、网络、进程间通信、主机号、进程号以及用户权限的隔离。

在同一个namespace下的进程可以感知彼此的变化,而对外界的进程一无所知。这样就可以让容器中的进程产生错觉,仿佛置身于一个独立的系统环境中,从而达到独立和隔离的目的。

1、分层docker镜像是采用分层的方式构建的,每个镜像都由一系列的“镜像层”组成。分层结构是docker镜像如此轻量的重要原因。当需要修改容器镜像内的某个文件时,只对处于最上方的读写层进行变动,不覆写下层已有文件系统的内容,已有文件在只读层中的原始版本依然存在,但会被读写层中的新版本所隐藏。当使用docker commit提交这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过的文件。

2、写时复制docker镜像使用了写时复制(copy-on-write)策略,在多个容器之间共享镜像,每个容器在启动的时候并不需要单独复制一份镜像文件,而是将所有镜像层以只读的方式挂载到一个挂载点,再在上面覆盖一个可读写的容器层。在未更改文件内容时,所有容器共享同一份数据,只有在docker容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的老版本文件。写时复制配合分层机制减少了镜像对磁盘空间的占用和容器启动时间。

3、内容寻址在docker1.10版本后,docker镜像改动较大,其中最重要的特性便是引入了内容寻址存储(content-addressable storage)的机制,根据文件的内容来索引镜像和镜像层。与之前版本对每个镜像层随机生成一个UUID不同,新模型对镜像层的内容计算校验和,生成一个内容哈希值,并以此哈希值代替之前的UUID作为镜像层的唯一标识。该机制主要提高了镜像的安全性,并在pull, push,load和save操作后检测数据的完整性。另外,基于内容哈希来索引镜像层,在一定程度上减少了ID的冲突并且增强了镜像层的共享。对于凯子不同构建的镜像层,主要拥有相同的内容哈希,也能被不同的镜像共享。

4、联合挂载通俗的讲,联合挂载技术可以在一个挂载点同时挂载多个文件系统,将挂载点的原目录与被挂载内容进行整合,使得最终可见的文件系统将包含整合之后的各层的文件和目录。实现这种挂载技术的文件系统通常称为联合文件系统。联合挂载适用于将多个镜像层的文件系统挂载到一个挂载点来实现一个统一文件系统视图的路径,最下层存储驱动(aufs,overlay等)实现分层合并的方式。所以严格来说,联合挂载并不是docker镜像的必须技术,比如在使用device mapper存储驱动时,其实是使用了快照技术来达到分层的效果。

docker镜像元数据:

Docker在管理镜像层元数据时采用的是从上至下repositry、image和layer三层。

1、repository元数据repository.json存储了所有本地镜像的repository的名字,还有每个repository下的镜像的名字、标签及其对应的镜像ID。当前docker默认采用SHA256算法根据镜像元数据配置文件计算出镜像ID。

2、image元数据包含了镜像架构(如amd64)、操作系统(如linux)、镜像默认配置、构建该镜像的docker版本、构建镜像的历史信息以及rootfs组成。docker利用rootfs中的diff_id计算出内容寻址的索引(chainID)来获得layer相关信息,进而获取每一个镜像层的文件内容。

3、layer元数据Docker中定义了Layer和RWLayer两种接口,分别用来定义只读层和读写层的一些操作,又定义了roLayer和mountedLayer,分别实现了上述两种接口。其中,roLayer用于描述不可改变的镜像层,mountLayer用于描述可读写的容器层。

具体来说,roLayer存储的内容主要有索引该镜像层的chainID,该镜像层的校验码diffID,父镜像层parent、graphdriver存储当前镜像层文件的cacheID、该镜像层size等内容。

再layer的所有属性中,diffID采用SHA256算法,基于镜像层文件包的内容计算得到,而chainID是基于内容存储的索引,它是根据当前层与所有祖先镜像层diffID计算出来。

如果该镜像层是最底层(没有父镜像层),该层的diffID便是chainID。

该镜像层的chainID计算公式为chainID(n)=SHA256(chain(n -1) diffID(n)),也就是根据父镜像层的chainID加上一个空格和当前层的diffID,再计算SHA256校验。

cgroup:

控制组可以提供对容器的内存、CPU、磁盘IO等资源进行限制和计费管理。具体来看,控制组提供:

资源限制:可以将组设置为不超过设定的内存限制。比如:内存子系统可以为进程组设定一个内存使用上限,一旦进程组使用的内存达到限额再申请内存,就会出发Out of Memory警告。

优先级:通过优先级让一些组件优先得到更多的CPU等资源。

资源审计:cgroups可以统计系统的资源使用量,如CPU使用时长、内存使用量等,这个功能非常适用于计费。

隔离:为组隔离命名空间,这样一个组不会看到另一个组的进程、网络连接和文件系统。

控制:挂起、恢复和重启等操作。

UnionFS:

联合文件系统(UnionFS)是一种轻量级的高性能分层文件系统,它支持将文件系统中的修改信息作为一次提交、并层层叠加,同时可以将不同目录挂载到同一个虚拟文件系统下,应用看到的是挂载的最终结果。

rootfs是什么:

rootfs是docker容器在启动时内部进程可见的文件系统,即docker容器的目录。rootfs通常包含一个操作系统运行所需的文件系统,例如可能包含典型的类Unix操作系统的目录系统,如/dev,/proc, /bin, /etc, /lib, /usr, /tmp 及运行docker容器所需的配置文件、工具等。

overlay2是什么:

Overlay2是文件存储驱动,基于内核overlayfs的Multiple lower layer特性实现,不在需要硬连接,直接将镜像层的各个目录设置为overlayfs的各个lower layer即可。

Overlay2将lowerdir、upperdir、workdir联合挂载,形成最终的merged挂载点,其中lowerdir是镜像只读层,upperdir是容器可读可写层,workdir是文件系统工作的基础目录,挂载后目录会被清空。

用户在写文件时,如果文件来自layer层,那直接写入即可。但是如果文件来自lower层,由于lower层文件无法修改,因此需要先复制到upper层,然后再往其中写入内容,这就是overlayfs的写时复制(copy-up)特性。

Overlayfs的lower layer文件写时复制机制让某一个用户再修改来自lower层的文件不会影响到其他用户,但是这个文件的复制动作会显得比较慢,后面我们会看到为了保证文件系统的一致性,这个copy-up包含了很多步骤,其中最为耗时的就是文件数据块的复制和fsync同步。用户再修改文件时,如果文件较小那可能不一定感受出来,但是当文件比较大或一次对大量的小文件进行修改,那耗时将非常可观。虽然自Linux-4.11起内核引入了“concurrent copy up”特性来提高copy-up的并行性,但是对于大文件也没有明显的效果。不过幸运的是,如果底层的文件系统支持reflink这样的延时拷贝技术(例如xfs)那就不存在这个问题了。

docker文件的结构是什么:

docker stop container

docker rm container

docker rmi image

docker pull image

docker run ...

Docker启动一个容器的过程:

docker cli命令通过API跟Docker Engine交互,然后Docker Engine调用OCI runtime(runc)来启动container。

containerd是容器运行时相关程序,docker engine 通过grpc调用containerd来运行容器,containerd最后通过runC来运行容器。

1、Create阶段:对client提交的POST表单进行分析整理,获取具有可移植性的配置结构体hostconfig。然后daemon会调用daemon,newContainer函数来创建一个基本的container对象,并将config和hostconfig中保存的信息填写到container对象中。

2、Start阶段:client紧接着会发送start请求来启动一个真正的物理容器。当Docker daemon接收到这个start请求后,会使用再create阶段配置好的container对象中的各种配置参数来完成volume挂点的注册,容器网络的创建和创建并启动物理容器等工作。

a.在Docker daemon启动之后,会创建一个默认的network,其本质工作就是创建了一个名为docker0的默认网桥。

b.确定默认网桥之后,daemon会调用container.BuildCreateEndpointOptions来创建此容器中endpoint的配置信息。然后再调用Network.CreateEndpont使用上面配置好的信息创建endpoint。

c.接下来daemon会调用daemon.buildSandboxOptions来创建此容器的sandbox,然后调用Network.NewSandbox来创建此容器的sandbox。

d.最后,daemon会调用ep.Join(sb)将endpoint加入到容器对应的sandbox中。

3.在完成创建容器的各种准备工作之后,Docker daemon会通过对libcontainer的一系列调用来完成容器创建和启动工作。Libcontainer时Docker的运行时库,它可以通过调用者提供的配置参数来创建并运行一个容器出来

a.创建逻辑容器和逻辑进程。所谓的逻辑容器container和逻辑进程process并非时真正进行着的容器和进程,而是libcontainer中所定义的结构体。逻辑容器container中包含了namespace, cgroups, device和mountpoint等各种配置信息。逻辑进程process中则包含了容器中所要运行的指令以其参数和环境变量等。

b.启动逻辑容器。Docker daemon会调用linuxContainer.Start来启动逻辑容器。

c.创建物理容器。容器中的init进程首先会调用StartInitiallization()函数,通过管道从父进程接收各种配置参数,然后对容器进行如下配置:

1.将init进程加入其指定的namespace中,这里会将init进程加入到前面已经创建好的netns中,这样init进程就拥有了自己独立的网络栈,完成了网络创建和配置的最后一步。

2.设置进程的会话ID。

3.使用系统调用,将前面注册好的挂载点全部挂载到物理主机上,这样就完成了volume的创建。

4、对指定目录下的文件系统进行挂载,并切换根目录到新挂载的文件系统下。设置hostname,加载profile信息。

5、最后使用exec系统调用来执行用户所指定的在容器中运行的程序。

Kubelet创建容器:

当kubelet要创建一个容器时,需要以下几步:

kubelet通过CRI接口(gRPC)调用dockershim,请求创建一个容器。CRI即容器运行时接口(Container Runtime interface),这一步中,kubelet可以视作一个简单的CRI Client,而dockershim就是接收请求的Server。目前dockershim的代码其实时内嵌在Kubelet中的,所以接收调用的凑巧就是kubelet进程;

dockershim收到请求后,转化成Docker Daemon能听懂的请求,发到Docker Daemon上请求创建一个容器。Docker Daemon早在1.12版本中就已经将针对容器的操作移到另一个守护进程——containerd中了,因此Docker Daemon仍然不能帮我们创建容器,而是要请求containerd创建一个容器。

containerd收到请求后,并不会自己直接去操作容器,而是创建一个叫做containerd-shim的进程,让containerd-shim去操作容器,这是因为容器进程需要一个父进程来做诸如收集状态,持续stdin等fd打开等工作。而加入这个父进程就是containerd,那每次containerd挂掉或升级,整个宿主机上所有的容器都得推出了。而引入了containerd-shim就规避了这个问题。

我们知道创建容器需要一些设置namespaces和cgroups,挂载root filesystem等等操作,而这些事该怎么做已经有了公开得规范了,那就是OCI。它得一个参考实现叫做runC。于是,containerd-shim在这一步需要调用runC这个命令行工具,来启动容器。

runC启动完容器后本身会直接退出,containerd-shim则会成为容器进程的父进程,负责收集容器进程的状态。上报给containerd,并在容器中pid为1的进程退出后接管容器中的子进程进行清理,确保不会出现僵尸进程。

K8s资源对象:

Master、Node、pod、labels、replication Controller、deployment、HPA、StatefulSet、Service、Job、Volumes、namespace、configmap。

K8s创建Pod:

开发岗面试汇总_第1张图片

 1、用户通过kubectl命名发起请求。

 2、apiserver通过对应的kubeconfig进行认证,认证通过后讲yaml中的pod信息存到etcd。

 3、Controller-Manager通过apiserver的watch接口发现了pod信息的更新,执行该资源所依赖的拓扑结果整合,整合后将对应信息写到etcd,此时pod已经可以被调度了。

4、Scheduler同样通过apiserver的watch接口更新到pod可以被调度,通过算法给pod分配节点,并将pod和对应节点绑定的信息写到etcd,然后将pod交给kubelet。

5、kubelet收到pod后,调用CNI接口给pod创建pod网络,调用CRI接口去启动容器,调用CSI进行存储卷的挂载。

6、网络,容器,存储创建完成后pod创建完成,等业务进程启动后,pod运行成功。

K8s网络模型:

k8s网络要解决的问题:1.容器与容器的通信;2.Pod到Pod的通信;3.Pod与Service的通信;4.集群外部与内部组件的通信。

k8s设计的基础原则:每个Pod都有一个独立的Ip地址;所有Pod都在一个可以直连、扁平的网络空间中;Pod要求可以直接通过对方ip进行访问。

k8s对集群网络的要求:1)所有容器都可以在不用NAT的方式下同别的容器通信;2)所有节点都可以在不用NAT的方式下同所有容器通信,反之亦然;3)容器的地址和别人看到的地址是同一个地址

k8s内容器到容器的通信时怎么做的:同一个Pod内的容器共享同一个netns,所以各类网络操作可以直接操作,就像在同一台机器上。

k8s内pod是怎么通信的:分为两种情况,在同一个node上Pod的通信和不同node上的pod通信。

1、对于同一个node上的pod,他们都通过Veth连接到同一个docker0网桥上,IP地址都是从docker0的网段上动态获取的,他们和网桥本身在同一个网段上,因此可以直接通信。

2、对于不同node上的pod,只能通过宿主机的网卡进行,于是需要满足两个条件:

a.在整个k8s集群中对Pod进行IP分配,不能有冲突——Flannel管理资源分配;

b.将pod的ip与node的ip关联起来

k8s中pod的ip数据流的目标是哪里:使用pause容器,将Pod里面的所有容器都连接到pause容器上,所有应用容器的端口映射都到pause容器上。

k8s中service和pod怎么通信:使用kube-proxy服务。Service在多个pod之间抽象一些服务,而且可以在同一个service创建的pod中做负载均衡。kube-proxy为每个新创建的服务都关联一个随机端口号,并创建负载均衡对象,直接和负载均衡到的pod进行网络交互。

CNI网络模型:

一种容器网络规范。包含容器和网络两种概念,对容器网络的设置都通过插件来实现。CNI插件包括两种类型:CNI Plugin和IPAM。CNI Plugin负责为容器配置网络资源,IPAM负责对容器IP地址进行分配管理。

cni plugin提供哪些操作:ADD,DELETE,CHECK,VERSION

直接路由:方式:让机器知道对端docker0的地址在哪里,然后让docker0互相通信,这样所有Node上运行的Pod就可以互相通信了。

通过部署MultiLayer Switch(MLS):在MLS中配置每一个docker0子网地址到Node地址的路由项,通过MLS将docker0的IP寻地址定向到对应的Node上。

将docker0和Node的匹配关系配置在Linux操作系统的路由项中(route add 命令添加静态路由规则),这样通信发起的Node就能根据这些路由信息直接找到目标Pod所在的Node,将数据传输过去。

动态路由发现协议来同步变化:在运行动态路由发现协议代理的Node时,将本机Local路由表的ip地址通过组播协议发布出去,同时监听其他Node的组播包。通过这样的信息交换,Node上的路由规则就能互相学习了。

Flannel:

flannel是一个专为kubernetes定制的三层网络解决方案,主要用于解决容器的跨主机通信问题,包括两点:

1)协助k8s,给每个node上的docker容器都分配互相不冲突的IP地址

2)在这些IP地址之间建立一个Overlay网络,通过这个网络,将数据包原封不动的传递到目标容器。

首先,flannel利用kubernetes API或者etcd用于存储整个集群的网络配置,其中最主要的内容为设置集群的网络地址空间。例如,设定整个集群内所有容器的IP取自网段“10.1.0.0/16”.

接着,flannel在每个主机中运行flanneld作为agent,

它上连etcd,利用etcd来管理可分配的IP地址资源,同时监控etcd中每个pod的实际地址,并在内存中建立一个Pod节点路由表;Flannel使用集中的etcd存储,每次分配的地址段都在同一个公共区域获取,而且在flannel分配好地址段之后,后面的事情由Docker完成,Flannel通过修改Docker的启动参数将分配给它的地址段传了出去。

它下连docker0和物理网络,使用内存中的Pod节点路由表,将docker0发给他的数据包包装起来,利用物理网络的连接(例如udp,vxlan等等)将数据包投递给目标flanneId上,从而完成Pod到Pod之间的直接地址通信。

容器监控:

IaaS指标:

CPU:cpu Usage, cpu node

memory: memory usage、memory node

disk:traffic,lops

network:进出带宽、进出流量、进出包量。

雪崩效应:在k8s中,有一个request和limit概念,如果request limit不配置,当一个pod跑到很高的情况下,会出现雪崩的效应,比如跑挂一台机器,这时候挂了之后,节点异常,k8s会自动地把这台机器上所有地Pod踢走,Pod会自动创建到另外地机器上,继续拖垮另外一台机器,这种可以称之为“雪崩效应”。最后造成地结果是k8s集群不可用。

k8s存储模型:

在docker容器中,为了实现数据地持久性存储,在宿主机和容器内做映射,可以保证在容器地生命周期结束,数据依旧可以实现持久性存储。

在k8s中,对于在同一节点地pod,可以使用本地数据卷来进行持久化存储。有三种方式:1)emptyDir: Pod挂载在本地地磁盘或者内存,被乘为emptyDir,称为临时空目录,随着Pod删除,也会被删除。适用于pod中容器之间的数据共享。

1. gitrepo目录:只是emptyDir上补添一个git命令来拉取文件而已。当pod创建时候,会拉取git(依赖于宿主机git命令驱动)仓库中数据克隆到本地,并且作为存储卷定义在pod之上。gitrepo基于emptyDir,此存储卷是emptyDir,git仓库中拉取得代码存放在emptyDir后被定义到pod。

2、hostPath模型:映射node文件系统中得文件或者目录到pod里,可实现针对某一节点的数据持久化,如果节点宕机了,那数据就丢失了。应用于Pod中容器需要访问宿主机。

对于分布在不同节点的pod,并不能实现不同节点之间持久性数据的共享,并且,在节点故障时,可能会导致数据的永久性丢失。为此,k8s就引入了外部存储卷的功能,通常是PVC和PV组合使用。PV是对底层网络共享存储的抽象,将共享存储定义为一种“资源”。而PVC是用户对存储资源的一个“申请”,用户在创建应用时定义好需要的PVC,PVC会去找k8s当前可用的PV,进行绑定,使用完后进行清理和释放。

PV定义:Storage:存储能力;accessMode:访问模式(分为RWD,ROX,RWX三种,不同存储提供商有不同的访问模式。)三种,不同存储提供商有不同的访问模式;storageClass:存储类型,当定义StorageClass的时候可以用做动态绑定;ReclaimPolicy:回收策略,有保留、回收空间和删除三种。

PV的生命周期:Available:可用;Bound:与PVC绑定;Released:绑定的PVC已删除;Failed:自动资源回收失败。

PVC和PV的生命周期:资源供应:静态和动态两种方式来创建PV;资源绑定:系统根据PVC对存储资源的请求,在已存在PV中选择一个绑定,如果没有PVC会一直pending,直到有符合要的PV。资源使用:Pod根据volume定义,将PVC挂载到容器内某个路径进行使用。资源释放:用户删除PVC,与PVC绑定的PV会标记为“已释放”。资源回收:如何处理遗留数据。

k8s中PV的创建:静态模式:管理员手工创建PV,定义他的属性;缺点:当PVC申请的资源比PV的资源少时,整个PV空间都会被该PVC占据,造成资源浪费;动态模式:管理员定义StorageClass,描述后端存储,标记为某种类型,比如Fast,Standard,Slow。优势:通过申明StorageClass和PVC完成资源绑定。系统在为PVC找到合适的StorageClass后,将自动创建一个PV并完成与PVC的绑定,没有浪费资源。一个PV只能被一个PVC使用,一个PVC可被多个pod使用。

Pod:

每个Pod都有一个pause容器和多个业务容器,多个业务容器共享pause容器的ip和volume。属于同一个pod的容器仅通过localhost就可以通信。

Pod的生命周期:pending,running,Succeeded,failed, unknown。

pod的探针:LivenessProbe:判断容器是否Running。ReadinessProbe:判断容器是否Ready,达到Ready状态的Pod才能接收请求。

Pod怎么检测容器健康状态:ExecAction:在容器内部执行命令,返回为0则说明状态;TCPSocketAction:通过容器IP地址和端口TCP检查,能建立TCP连接说明容器健康;HTTPGetAction:对容器IP地址,端口和路径调用HTTP Get方法,返回200-400说明健康

Pod如何滚动升级:直接再Deployment定义中,通过spec.strategy指定pod更新的策略,Recreate,RollingUpdate。第二种:使用kubectl rolling-update命令做滚动升级,滚动升级通过执行kubectl rolling-update命令一键完成,该命令创建一个新的RC,然后自动控制旧的RC中的Pod副本数量逐渐减少到0,同时新的RC中的Pod副本的数量从0逐步增加到目标值,最终实现了Pod的升级。

Pod如何回滚:使用kubectl rollout history命令检查部署历史记录:使用kubectl rollout pause命令暂停Deployment更新操作的;修改信息使用kubectl rollout resume deploy命令恢复部署操作;使用kubectl rollout undo deployment nginx-deployment --to-revision=5命令回退到指定版本。

访问Pod:hostPost:某个容器端口映射到物理机上。使用物理机IP+hostPort访问某个容器;hostNetwork:true:Pod所有容器映射到物理机。

endpoint:pod ip + containerPort

statefulSet使用场景:有状态的服务,比如zk集群、mysql集群等等。1.每个节点固定ID。2.集群的规模固定;3.每个节点都是由状态的,会持久化数据到永久存储中。4.磁盘不能损坏,否则不能提供服务。

StatefulSet的特征:1.每个Pod都有唯一稳定的网络标识符:(每一个Pod实例都有一个唯一的DNS域名,可以直接访问);2.Pod副本的启停是受控的,操作第n个Pod时,前n-1的Pod都是ready的;3.Pod采用稳定的持久化存储券PV+PVC,删除pod不会删除相关的存储券;

Headless Service没有Cluster IP,解析DNS域名是返回的所有Pod的Endpoint列表。

Service:

service到后端pod由哪些LB策略:RoundRobin:轮询转到后端Pod;SessionAffinity:基于客户端IP的会话保持,相同客户端会到相同Pod。

服务发现:每个Service都配置了一个全局唯一的Cluster IP和名称,再Service整个生命周期内,ClusterIP不发生改变。因此,服务发现可以用Service的Name和Cluster IP地址做DNS域名映射。

DNS服务:使用CoreDNS,使用不同的插件来增强kube-dns中的功能。

1、修改每个Node上kubelet启动参数并重启kubelet;

2、创建CoreDns应用;configmap:设置coreDNS主配置文件Corefile内容;Deployment:设置CoreDNS容器应用的内容;Service:DNS服务的配置,设置固定ClusterIP。

外部系统怎么访问service:由于ClusterIP是一个虚拟IP,只能再集群内部访问,不能对外暴露。因此,可以采用NodePort方式,再集群的每个Node都为需要外部访问的Service开启一个对应的TCP监听端口,外部系统只要用任意一个Node的IP地址+NodePort端口号就可以访问服务。

ConfigMap:

配置文件中的参数在容器运行时如何修改。Docker提供通过Docker Volume将容器外的配置文件映射到容器内的方式,但需要首先把配置文件拷贝到主机上,在分布式环境下要做到文件一致性比较复杂。

1、定义ConfigMap资源对象,在data域中声明k-v的配置项,将整个Map持久化到Etcd中。

2、将存储在Etcd中的ConfigMap通过Volume映射的方式挂载到目标Pod内。无论Pod调度到哪台node上,都完成自动映射。

3、在Pod使用时,可以通过环境变量(env)或者volumes来使用。

ConfigMap热更新:1.环境变量或init-container中使用configMap不能热更新,2.configmap作为volume进行挂载时可以热更新,每次进行Pod同步时(默认10秒一次);3.使用subPath将configmap挂载到其他目录无法热更新;

1、针对可以热更新的场景,如果由实时性要求,就在应用内监听本地文件的变化,在文件变更时触发一次配置升级

2、对于不能热更新的场景,滚动更新Pod。方法一:使用controller来监听configMap变更触发滚动升级。方法二:修改pod annotation强制触发滚动升级

API Server:

提供k8s各类资源对象的增删改查watch等REST接口。它通过在Master上kube-apiserver进程提供服务。

API层:以REST方式提供各种接口

访问控制层:鉴权、验证身份等

注册表层:资源对象保存在注册表

etcd数据库

List-watch机制:1、etcd提供了watch接口,API server可以watchetcd上发生的数据操作

2、API server模仿etcd的watch API提供自己的watch接口,k8s的其他组件可以在API server上监听自己感兴趣的资源。

3、可以实现数据同步。客户端调用API server 的List接口将数据缓存到本地(HTTP短链接)。然后启动对应资源的watch写成,在收到watch事件后,对内存中的全量资源做同步修改

watch实现:使用分块传输编码。当客户端调用Watch API时,API server在response的HTTP header中设置Transfer-Encoding为chunked,客户端收到消息后,和服务端复用这个连接,等待下一个数据块。

消息的可靠性、实时性和顺序性:

可靠性:list和watch保证。List查询当前资源状态,客户端可以比较期望状态和当前状态,纠正不一致资源。

实时性:每当API Server资源产生状态变更事件,都会将事件及时推送给客户端。

顺序性:k8s在每个资源事件中都带上了递增的resourceVersion标签,当客户端并发处理同一个资源事件,就可以比较resourceVersion保证最终状态一致。

高性能:watch使用分块传输的方式复用一条长连接。
k8s各模块通信:API Server作为集群的核心,负责各模块的通信,集群内模块通过API server把信息存入etcd,需要操作时,通过REST接口实现模块的信息交互。
kubelet与API server:1)每个Node上的kubelet每隔一定周期,调用一次API Server接口报告自身状态;2)kubelet通过API server接口watch Pod信息,如果Pod绑定在本node,就执行相应容器创建和初始化工作

kube-scheduler-manager与API:通过API server的watch接口监听到新建Pod信息后,检索所有符合要求的Node,执行Pod调度机制,将Pod绑定到node上。

Scheduler:

将调度的Pod按照特定的调度算法和调度策略绑定到集群集群中某个合适的node上,并将绑定信息写到etcd。

1、预选调度:遍历所有目标node,根据预选策略,筛选出符合要求的候选节点

2、确认最优节点:采用优选策略计算每个候选节点的积分,积分最高胜出

Predicates:

PodFitPorts:没有任何端口冲突

PodFitsResurece:由足够的资源运行Pod

NoDiskConflict: 由足够的空间来满足Pod和链接的数据卷

MatchNodeSelector:能够匹配Pod中的选择器查找参数

HostName:能够匹配Pod中的Host参数

Priorities:

LeastrequestPriority:计算Pods需要的CPU和内存在当前节点可用资源的百分比

BalanceResourceAllocation:拥有类似内存和CPU使用节点

ServicesSpreadingPirority:优先选择拥有不同Pods的节点

EqualPirority:给所有集群的节点同样的优先级,仅仅是为了测试

Pod的controller:

1、Replication:控制有特定的副本运行,若大于则特定的数量就kill,若少于特定数量则create。

 注意:

    1)Replication Controller只会对RestartPolicy = Always的Pod的生效(RestartPolicy的默认值就是Always),Replication Controller 不会去管理其他启动策略pod。

    2)Replication Controller永远不会自己关闭。

2、ReplicaSet:

        Replication只支持等式的selector,但是ReplicaSet还支持新的,基于集合的selector。

3、Deployment:

        Deployment为Pod和Replica Set提供声明式更新。

4、StatefulSet:

          无状态服务。

           应用场景:

                1)稳定的持久化存储(Pod重新调度后还是能访问到相同的持久化存储,基于PVC实现)。

                2)稳定的网络标示(Pod重新调度后其PodName和HostName不变,基于Headless service)

                3) 有序扩容、有序部署。

                4)有序扩容、有序删除。

5、DaemonSet:

        保证在每个Node上都运行一个容器副本,常用来部署一些集群的日志、监控或其他系统管理应用。

有状态服务。

应用场景:

1)稳定的持久化存储(Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现)

2)稳定的网络状态(Pod重新调度后)

Controller实现高可用:

etcd锁实现:

步骤1:准备

客户端连接Etcd,以/lock/mylock为前缀创建全局唯一key

步骤2:创建定时任务作为租约的“心跳”

当一个客户端持有锁期间,其他客户端只能等待,为了避免等待期间租约失效,客户端需创建一个定时任务作为“心跳”进行租约。此外,如果持有锁期间客户端崩溃,心跳停止,key将因租约到期而被删除,从而锁释放,避免死锁。

步骤3:客户端将自己全局唯一的key写入Etcd

进行put操作,将步骤1中创建的key绑定租约写入Etcd,根据Etcd的Revision机制,假设两个客户端put操作返回的Revision分别为1、2;客户端记录Revision用以接下来判断自己是否能获得锁。

步骤4:客户端判断是否获得锁

客户端以前缀/lock/mylock读取keyvalue列表;判断自己key的Revision是否为当前列表最小的,如果是则认为获得锁;否则监听列表中一个Revision比自己小的key的删除事件,一旦监听到删除事件或者因租约失效而删除的事件,自己获得锁

步骤5:执行业务

获得锁后,操作共享资源,执行业务代码。

步骤6:释放锁

完成业务流程后,删除对应得key释放锁。

revision功能:通过Revision得大小就可以进行写操作得顺序。在实现分布式锁时,多个客户端同时枪锁,根据Revision号大小一次获得锁,可以避免“羊群效应”,实现公平锁。

运维:

网络:

OSI七层协议、IP是哪一层、TCP是哪一层、TCP得冲突和数据链路层冲突区别:

应用层:网络服务与终端用户得一个接口。

协议有:HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP

表示层:数据的表示、安全、压缩。(在五层模型里面已经合并到了应用层)

格式有:JPEG、ASCII、EBCDIC、加密格式等

会话层:建立、管理、终止会话。(在五层模型里面已经合并到了应用层)

传输层:定义传输数据的协议端口号,以及流控和差错校验

协议有:TCP UDP

网络层:进行逻辑地址寻址,实现不同网络之间的路径选择。

协议有:ICMP、IGMP、IP(IPV4/IPV6)

数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能。(由底层网络定义协议)

将比特组合成字节而组合成帧,用MAC地址访问介质。错误发现但不能纠正。

物理层:建立、维护、断开物理连接。(由底层网络定义协议)

http2.0特点:

为了解决Http1.0+协议存在的问题,并且提高网络传输性能、优化网络传输过程。

二进制分帧:http2.0会将所有传输的消息分割成为更小的消息和帧,并对它们采用二进制表格的编码,其中http1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。

当我们建立连接的时候,就可以承载任意数量的双向数据流,每一个数据流都以消息的形式发送,消息有一个或多个帧组成,可乱序发送。每个帧首部都会有一个标识位,接收到之后就可以重装。

多路复用:可以发送多个请求,可以不用收到回复就继续发送请求。

优点:并行交错发送请求,请求之间互不影响

           TCP连接一旦建立可以并行发送请求

           消除不必要延迟,减少页面加载时间

           可以最大程度利用HTTP 1.x

首部压缩:首部压缩可以使得头部帧可以最大程序复用,减少头部的大小,有利于减少内容和流量。比如我们第一次发送请求,里面包含头部的各种信息;但是后来我们又发送另外的请求,发现大部分的字段可以复用的,我们只要发送一个当前请求特有的头部帧即可。由于首部表在Http2.0的连接存储期内始终是有效的,客户端和服务端共同更新。

流量控制:Http2.0“流”的流量控制最终的目标是在不改变协议的情况之下允许采用多种流量控制算法:

流量基于HTTp连线的每一跳进行,非端到端控制

流量基于窗口更新帧进行,接收方可广播准备接收字节数甚至对整个连接要接收的字节数

流量控制有方向性,接收方可以根据自身情况进行控制窗口大小

流量控制可以由接收方禁用,包括个别流和整个连接

只有DATA帧服从流量控制,其他类型帧不会消耗控制窗口的空间

请求优先级:客户端可以通过在打开流的HEADERS帧中包含优先次序信息来为一个新流指定优先级。在其任意时间,可以使用PRIORITY帧来改变流的优先级。0表示最高优先级,-1表示最低优先级。

服务器可以根据流的优先级控制资源分配(CPU、内存、宽带),而在响应数据准备好之后,优先将最高优先级的帧发送给客户端。

服务器推送:一般HTTP请求都是由客户端发起,服务器收到请求进行返回。但是HTTP2.0可以服务器主动返回资源给客户端用户。提高性能。

tcp长短连接应用场景:

长连接:多用于操作频繁,点对点的通讯,而且连接数不能太多的场景。

例如:扫码登陆,聊天室,数据库连接

短链接:用户无需频繁操作,不需要一直获取服务端反馈情况下需要短链接就好。

WEB网站的http服务一般都是短链接,因为长连接对于服务端来说会消耗一定的资源。

tcp挥手中time-wait为什么是两倍时间:

当主动关闭方发送完最后一个ACK包后无法确认对方是否有收到这个ACK包,所以2MSL的时间能保证如果对方没收到会重发第三次挥手的FIN包,且这个包有足够的时间发送回来。如果每发送回来,便证明对方已经收到了ACK包;而且等待2MSL时间,任何迟到的报文段都将被丢弃,也不会产生垃圾报文。

常用的网络工具:ping,tcpdump,mtr,dig:

ping:利用网络上机器IP地址地唯一性,给目标IP地址发送一个数据包,再要求对方返回一个同样大小地数据包来确定两台网络机器是否连通以及时延是多少。

tcpdump: 请求情况,最近的tcp连接到网关veth的请求情况;ip端口,收到,ack值

mtr:结合了ping+traceroute+nslookup;

         第一列:HOST,显示IP地址或者主机名

         第二列:Loss%,这个节点的丢包率

         第三列:Snt,发送包的数量

         第四列:Last,最近一次的延时,单位是毫秒ms。

         第五列:Avg,平均延时,单位是毫秒ms。

         第六列:Best,最低延时,单位是毫秒ms.

         第七列:Wrst,最高延时,单位是毫秒ms.

         第八列:StDev,标准偏差。

dig:常用的域名查询工具,可以用来测试域名系统工作是否正常。

A地址记录,用来指定域名的IPv4地址,如果需要将域名指向一个IP地址,就需要添加A记录。

AAAA用来指定主机名(或域名)对应的IPv6地址记录。

CNAME如果需要将域名指向另一个域名,再由另一个域名提供ip地址,就需要添加CNAME记录。

lvs工作模式:

dr:直接路由模式、tun:隧道模式,nat:路由转发模式、fullnat模式

NAT:用户在浏览器端输入域名,通过DNS域名解析出VIP,然后通过虚拟IP找到LVS,LVS是一个调度者,在LVS的后端有个Nginx集群,这个集群处于企业内部的内网,在内网里面是互通的,但是外部无法直接访问的,集群中每个Nginx的响应都会返回给LVS,最后再由LVS返回给用户。

TUN:用户的请求通过LVS道道NGINX集群,请求的过程和NAT一样;服务器的响应不会经过LVS,而是直接返回给用户,这个时候LVS的压力就大大的降低了;

DR:与TUN模式不同的是,Nginx集群的响应会同一经过一个路由中转之后,统一返回给用户,此时Nginx集群处理私网当中的;

linux权限:

数字的三位分别代表:当前用户、群组用户、其他用户。

权限数字代表:读r=4,写w=2,执行x=1

你可能感兴趣的:(读书笔记,面试)