1、C++11的新特性
(1)新的关键字
auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作。auto实际上实在编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响。另外,似乎auto并不会影响编译速度,因为编译时本来也要右侧推导然后判断与左侧是否匹配。
decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型。
nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0
(2)lambda表达式
lambda表达式类似Javascript中的闭包,它可以用于创建并定义匿名的函数对象,以简化编程工作。Lambda的语法如下:
[函数对象参数](操作符重载函数参数)->返回值类型{函数体}
下面是各种变量截取的选项:
(3)变长参数的模板
由于在C++11中引入了变长参数模板,所以发明了新的数据类型:tuple,tuple是一个N元组,可以传入1个, 2个甚至多个不同类型的数据
(4)更加优雅的初始化方法
在C++11中,我们可以使用以下语法来进行替换:
2、C++的构造函数explict,用来干嘛的
explicit关键字的作用就是防止类构造函数的隐式自动转换.(https://www.cnblogs.com/ymy124/p/3632634.html)
3、C++多态的实现机制
(1)静态多态(重载,模板)
是在编译的时候,就确定调用函数的类型。
(2)动态多态(覆盖,虚函数实现)
在运行的时候,才确定调用的是哪个函数,动态绑定。运行基类指针指向派生类的对象,并调用派生类的函数。
虚函数实现原理:虚函数表和虚函数指针。
纯虚函数: virtual int fun() = 0;
那么,多态的作用是什么呢?我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。
而多态则是为了实现另一个目的——接口重用!而且现实往往是,要有效重用代码很难,而真正最具有价值的重用是接口重用,因为"接口是公司最有价值的资源。设计接口比用一堆类来实现这个接口更费时间。而且接口需要耗费更昂贵的人力的时间。"
https://www.cnblogs.com/Allen-rg/p/6927129.html
4、有用过什么设计模式
https://www.cnblogs.com/kubixuesheng/p/4355055.html
5、Linux系统里代码编译成汇编之后,指令和寄存器有哪些
http://blog.sina.com.cn/s/blog_87c063060101bcwt.html
6、Redis 里String类型有什么特色,Redis数据类型的应用场景
https://redisbook.readthedocs.io/en/latest/index.html
数据类型:
(1)简单动态字符串
sds:“包含字符串值的字符串对象”,这种说法初听上去可能会有点奇怪, 但是在 Redis 中, 一个字符串对象除了可以保存字符串值之外, 还可以保存 long
类型的值, 所以为了严谨起见, 这里需要强调一下: 当字符串对象保存的是字符串时, 它包含的才是 sds 值, 否则的话, 它就是一个 long
类型的值。
(2)双端链表
双端链表主要有两个作用:
双端链表及其节点的性能特性如下:
节点带有前驱和后继指针,访问前驱节点和后继节点的复杂度为 O(1) ,并且对链表的迭代可以在从表头到表尾和从表尾到表头两个方向进行;
链表带有指向表头和表尾的指针,因此对表头和表尾进行处理的复杂度为 O(1)
链表带有记录节点数量的属性,所以可以在 O(1)复杂度内返回链表的节点数量(长度);
(3)字典
字典的用途:
实现字典的方法有很多种:
在众多可能的实现中, Redis 选择了高效、实现简单的哈希表,作为字典的底层实现。
字典的 rehash 操作实际上就是执行以下任务:
ht[0]->table
更大的 ht[1]->table
;ht[0]->table
中的所有键值对迁移到 ht[1]->table
;ht[0]
的数据清空,并将 ht[1]
替换为新的 ht[0]
;经过以上步骤之后, 程序就在不改变原有键值对数据的基础上, 增大了哈希表的大小。
rehash完毕后执行:
ht[0]
的空间;ht[1]
来代替 ht[0]
,使原来的 ht[1]
成为新的 ht[0]
;ht[1]
;rehashidx
属性设置为 -1
,标识 rehash 已停止;(4)跳跃表
为了满足自身的功能需要, Redis 基于 William Pugh 论文中描述的跳跃表进行了以下修改:
score
值:多个不同的 member
的 score
值可以相同。score
值,还要检查 member
:当 score
值可以重复时,单靠 score
值无法判断一个元素的身份,所以需要连 member
域都一并检查才行。应用:
和字典、链表或者字符串这几种在 Redis 中大量使用的数据结构不同, 跳跃表在 Redis 的唯一作用, 就是实现有序集数据类型。
7、多路IO复用,select和epoll的区别,Epoll相对来说的好处
引入:
支持高并发连接.官方测试的是5w并发连接但在实际生产中可制成2-4w并发连接数,得益于nginx使用最新的epoll(linux 2.6内核)和kqueue(freebsd)网络I/O模型.而apache使用的则是传统的select模型,其比较稳定的prefork模式为多进程模式,需要经常派生子进程,所消耗的CPU等服务器资源要比nginx高的多.
select 和epoll效率差的原因: select是轮询、epoll是触发式的,所以效率高。
select:
1.Socket数量限制:该模式可操作的Socket数由FD_SETSIZE决定,内核默认32*32=1024.
2.操作限制:通过遍历FD_SETSIZE(1024)个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍.
poll:
1.Socket数量几乎无限制:该模式下的Socket对应的fd列表由一个数组来保存,大小不限(默认4k).
2.操作限制:同Select.
epoll:
1.操作限制:同Select.
2.操作无限制:基于内核提供的反射模式,有活跃Socket时,内核访问该Socket的callback,不需要遍历轮询. 但是当所有Socket都活跃的时候,这时候所有的callback都被唤醒,会导致资源的竞争.既然都是要处理所有的Socket,那么遍历是最简单最有效的实现方式.
9、共享内存是怎么实现的
nmap函数要求内核创建一个新额虚拟存储器区域,最好是从地质start开始的一个区域,并将文件描述符fd指定对象的一个连续的片(chunk)映射到这个新的区域。
SHMMNI为128,表示系统中最多可以有128个共享内存对象。
一、内核怎样保证各个进程寻址到同一个共享内存区域的内存页面
1、page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。
2、文件与address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。
3、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。
4、对于共享内存映射情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。
注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。
5、所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
10、讲一下ARP协议运作的过程
ARP(Address Resolution Protocol,地址解析协议)是将IP地址解析为以太网MAC地址(或称物理地址)的协议。
在局域网中,当主机或其它网络设备有数据要发送给另一个主机或设备时,它必须知道对方的网络层地址(即IP地址)。但是仅仅有IP地址是不够的,因为IP数据报文必须封装成帧才能通过物理网络发送,因此发送站还必须有接收站的物理地址,所以需要一个从IP地址到物理地址的映射。APR就是实现这个功能的协议。
假设主机A和B在同一个网段,主机A要向主机B发送信息。具体的地址解析过程如下
(1) 主机A首先查看自己的ARP表,确定其中是否包含有主机B对应的ARP表项。如果找到了对应的MAC地址,则主机A直接利用ARP表中的MAC地址,对IP数据包进行帧封装,并将数据包发送给主机B。
(2) 如果主机A在ARP表中找不到对应的MAC地址,则将缓存该数据报文,然后以广播方式发送一个ARP请求报文。ARP请求报文中的发送端IP地址和发送端MAC地址为主机A的IP地址和MAC地址,目标IP地址和目标MAC地址为主机B的IP地址和全0的MAC地址。由于ARP请求报文以广播方式发送,该网段上的所有主机都可以接收到该请求,但只有被请求的主机(即主机B)会对该请求进行处理。
(3) 主机B比较自己的IP地址和ARP请求报文中的目标IP地址,当两者相同时进行如下处理:将ARP请求报文中的发送端(即主机A)的IP地址和MAC地址存入自己的ARP表中。之后以单播方式发送ARP响应报文给主机A,其中包含了自己的MAC地址。
(4) 主机A收到ARP响应报文后,将主机B的MAC地址加入到自己的ARP表中以用于后续报文的转发,同时将IP数据包进行封装后发送出去。
当主机A和主机B不在同一网段时,主机A就会先向网关发出ARP请求,ARP请求报文中的目标IP地址为网关的IP地址。当主机A从收到的响应报文中获得网关的MAC地址后,将报文封装并发给网关。如果网关没有主机B的ARP表项,网关会广播ARP请求,目标IP地址为主机B的IP地址,当网关从收到的响应报文中获得主机B的MAC地址后,就可以将报文发给主机B;如果网关已经有主机B的ARP表项,网关直接把报文发给主机B。
12、你了解分布式设计和开发是哪些
滴滴出行消息服务团队近日开源了其内部广泛使用的分布式消息中间件产品 DDMQ,这是一款致力于提供低延迟、高并发、高可用、高可靠消息服务的企业级消息队列产品。
DDMQ 具有如下的优秀特性:
低延迟高吞吐:毫秒级延迟,单机百万条消息吞吐。
丰富的消息类型:具备实时消息、延时消息和分布式事务消息。
海量消息存储,支持消息回溯消费:支持 RocketMQ 和 Kafka 作为实时消息的存储引擎,使用RocksDB 作为延时消息的存储引擎。
秒级延时消息:支持单条消息设置精确到秒级的延迟时间,提供普通延时消息和循环延时消息。
多语言客户端,提供了主流开发语言SDK,包括PHP, Java, Go, C/C++, Python,在API 上保持着最易使用的 High Level 形式。
多种消费方式:支持通过 Thrift RPC拉取、HTTP 推送和第三方存储直写的方式消费消息。
支持灵活的消息过滤和转换功能:通过使用 Groovy 脚本在服务端进行消息体的转换和过滤,能做有效减少客户端和服务器的数据传输量,减轻客户端处理消息的负载。
统一的Web控制台:方便用户管理Topic等资源,通过控制台可以实现配置生产和消费的限流值、消费方式、Groovy脚本、启停消费、重置消费进度等功能。
完善的监控配套:提供模块的健康检查和消息堆积告警功能。
适用场景;
消息队列作为构建现代分布式应用所必备的基础设施,有着广泛的应用场景。
削峰填谷
在秒杀等场景下会导致短时间流量的暴涨,下游系统会因为缺少保护而过载甚至崩溃。DDMQ提供的海量堆积能力和消费限流能够确保下游系统的平稳运行。
异步解耦
通过上下游系统的松耦合设计,可以保证上游系统不会因为下游系统的宕机而不可用。确保主流程的正常稳定运行。
顺序消息
现实中需要保证顺序的场景很多,比如订单系统中订单创建、支付、退款等流程,均需要保证顺序。 DDMQ提供的顺序消费功能可以保证消息的先进先出。
事务消息
在微服务的场景下,通过DDMQ的事务消息能够达到分布式事务的最终一致性
架构设计
下面这张图描述了DDMQ 的总体架构。主要包括 Broker Cluster、Producer Proxy Cluster(以下简称 PProxy),Consumer Proxy Cluster(以下简称CProxy),SDK,Console 等模块。
Broker Cluster 是DDMQ的消息存储层。使用 RocketMQ作为实时消息的存储引擎(同时也支持使用Kafka),Chronos则是我们基于 RocksDB自研的延时消息存储引擎。
PProxy 是DDMQ的生产代理服务, 内置 Thrift RPC Server,生产 SDK 通过RPC 调用将消息发送给 PProxy,然后再由PProxy负责将消息生产到具体的 Broker 中去,在 PProxy 中我们实现了生产限流、重试和消息批量生产等功能。
CProxy 是DDMQ的消费代理服务,也内置了Thrift RPC Server,当选择SDK消费时,消费方以 pull 的方式从 CProxy 中拉取消息,由于 CProxy 中的PullBuffer提前缓存了一定数量的待消费消息,因此消费的延迟很低。如果选择HTTP方式消费,则直接由CProxy将消息推送到业务指定的回调URL地址。在CProxy 中,我们实现了消息过滤(通过编写Groovy脚本)、消息体转换(Transit)、重试、消费限流、顺序消费内部排序等功能。
Console是DDMQ的控制台,用户通过控制台申请Topic、Group等资源。Topic等数据会持久化到MySQL并推送到 Zookeeper;PProxy和CProxy通过读取、监听 Zookeeper 上的Topic和Group 数据来实时控制消息的生产和消费逻辑。
DDMQ选择Proxy+SDK的架构,主要有这几个好处:
方便实现多语言SDK的实现,由于滴滴内部使用的技术栈比较多,将主要逻辑放在 Proxy 上有利于降低 SDK的复杂度,让SDK的开发速度大大加快。目前在滴滴内部支持PHP, Go , C/C++, Java, Python, Node.js等语言的SDK实现。
存储层业务无感知,由于Proxy层屏蔽了后面的RocketMQ或Kafka,使得存储层的切换可以做到业务无感知。
加快新功能迭代速度,新功能的开发都在 Proxy 层实现,降低了SDK的升级频率。
DDMQ 已经在滴滴内部稳定运行了两年多时间,支撑了网约车、小桔车服、地图、金融、智能驾驶、智慧交通、外卖等业务的稳定运行。日消息流水达到千亿级别,整体服务可用性超过5个9。
13、怎么做负载均衡
RocketMQ是一个分布式具有高度可扩展性的消息中间件。本文旨在探索在broker端,生产端,以及消费端是如何做到横向扩展以及负载均衡的。
Broker是以group为单位提供服务。一个group里面分master和slave,master和slave存储的数据一样,slave从master同步数据(同步双写或异步复制看配置)。
通过nameserver暴露给客户端后,只是客户端关心(注册或发送)一个个的topic路由信息。路由信息中会细化为message queue的路由信息。而message queue会分布在不同的broker group。所以对于客户端来说,分布在不同broker group的message queue为成为一个服务集群,但客户端会把请求分摊到不同的queue。
而由于压力分摊到了不同的queue,不同的queue实际上分布在不同的Broker group,也就是说压力会分摊到不同的broker进程,这样消息的存储和转发均起到了负载均衡的作用。
Broker一旦需要横向扩展,只需要增加broker group,然后把对应的topic建上,客户端的message queue集合即会变大,这样对于broker的负载则由更多的broker group来进行分担。
并且由于每个group下面的topic的配置都是独立的,也就说可以让group1下面的那个topic的queue数量是4,其他group下的topic queue数量是2,这样group1则得到更大的负载。
在集群消费模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个实例即可。RocketMQ采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue。
而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照queue的数量和实例的数量平均分配queue给每个实例。
默认的分配算法是AllocateMessageQueueAveragely
由于广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以也就没有消息被分摊消费的说法。
在实现上,其中一个不同就是在consumer分配queue的时候,会所有consumer都分到所有的queue。
14、负载均衡多进程的分配算法
Toeplitz Hash
1、轮询法
将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
2、随机法
通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,
其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。
3、源地址哈希法
源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。
4、加权轮询法
不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
5、加权随机法
与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。
6、最小连接数法
最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前
积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。
15、内存池怎么设计的,为什么设计内存池
内存池的作用在于消除频繁调用系统默认的内存分配和释放函数所带来的开销问题。
由于每次要求分配的内存大小不等,使用默认的内存分配函数的话,可能给系统带来大量的碎片问题,所以,将内存配置问题交给底层的内存池去处理,是一个不错的选择。
考虑一个常规的空间配置器(或者说内存池)的作用是什么?分配内存和释放内存。
使用了空间配置器后,我们将函数调用malloc/free、new/delete的工作包装到配置器中,然后内存池提供相关的分配内存和释放内存接口。这就是最简单的空间配置器
内存池细节考虑
由于每一次分配内存的大小不定,所以考虑到管理方便,STL内存池采用两级内存配置器,若要求的内存大于128bytes,使用malloc/free直接分配和回收。若小于等于128bytes时,由内存池分配和回收。
在内存池中,以8bytes为单位,即维护从8,16,24,..,128bytes的16个链表,每个链表上链接的是对应大小的区块。每一次分配内存时,先将要求的内存大小扩展为8的倍数,然后从对应链表上取走一个区块,释放时,将区块重新链接到对应链表上。
17、守护进程怎么设计的,父进程不是负责通知子进程么
Linux daemon是运行于后台常驻内存的一种特殊进程,周期性的执行或者等待trigger执行某个任务,与用户交互断开,独立于控制终端。一个守护进程的父进程是init进程,它是一个孤儿进程,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都被丢到了/dev/null中。守护进程一般用作服务器进程,如httpd,syslogd等。
用fork创建子进程,父进程退出,子进程成为孤儿进程被init接管,子进程变为后台进程。
调用setsid()让子进程成为新会话的组长,脱离父进程的会话期。setsid()在调用者是某进程组组长时会失败,但是A已经保证了子进程不会是组长,B之后子进程变成了新会话组的组长。
因为会话组的组长有权限重新打开控制终端,所以这里第二次fork将子进程结束,留着孙进程,孙进程不是会话组的组长所以没有权利再打开控制终端,这样整个程序就与控制终端隔离了。
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。
将三个标准文件描述符定向到/dev/null中
进程活动时,其工作目录所在的文件系统不能卸下(比如工作目录在一个NFS中,运行一个daemon会导致umount无法成功)。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如chdir("/tmp"),进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
18、打开网页比较详细的过程(dns解析过程)
什么是DNS解析?当用户输入一个网址并按下回车键的时候,浏览器得到了一个域名。而在实际通信过程中,我们需要的是一个IP地址。因此我们需要先把域名转换成相应的IP地址,这个过程称作DNS解析。
1) 浏览器首先搜索浏览器自身缓存的DNS记录。
或许很多人不知道,浏览器自身也带有一层DNS缓存。Chrome 缓存1000条DNS解析结果,缓存时间大概在一分钟左右。
2) 如果浏览器缓存中没有找到需要的记录或记录已经过期,则搜索hosts文件和操作系统缓存。
在Windows操作系统中,可以通过 ipconfig /displaydns 命令查看本机当前的缓存。
通过hosts文件,你可以手动指定一个域名和其对应的IP解析结果,并且该结果一旦被使用,同样会被缓存到操作系统缓存中。
Windows系统的hosts文件在%systemroot%\system32\drivers\etc下,linux系统的hosts文件在/etc/hosts下。
3) 如果在hosts文件和操作系统缓存中没有找到需要的记录或记录已经过期,则向域名解析服务器发送解析请求。
其实第一台被访问的域名解析服务器就是我们平时在设置中填写的DNS服务器一项,当操作系统缓存中也没有命中的时候,系统会向DNS服务器正式发出解析请求。这里是真正意义上开始解析一个未知的域名。
一般一台域名解析服务器会被地理位置临近的大量用户使用(特别是ISP的DNS),一般常见的网站域名解析都能在这里命中。
4) 如果域名解析服务器也没有该域名的记录,则开始递归+迭代解析。
这里我们举个例子,如果我们要解析的是mail.google.com。
首先我们的域名解析服务器会向根域服务器(全球只有13台)发出请求。显然,仅凭13台服务器不可能把全球所有IP都记录下来。所以根域服务器记录的是com域服务器的IP、cn域服务器的IP、org域服务器的IP……。如果我们要查找.com结尾的域名,那么我们可以到com域服务器去进一步解析。所以其实这部分的域名解析过程是一个树形的搜索过程。
根域服务器告诉我们com域服务器的IP。
接着我们的域名解析服务器会向com域服务器发出请求。根域服务器并没有mail.google.com的IP,但是却有google.com域服务器的IP。
接着我们的域名解析服务器会向google.com域服务器发出请求。...
如此重复,直到获得mail.google.com的IP地址。
为什么是递归:问题由一开始的本机要解析mail.google.com变成域名解析服务器要解析mail.google.com,这是递归。
为什么是迭代:问题由向根域服务器发出请求变成向com域服务器发出请求再变成向google.com域发出请求,这是迭代。
5) 获取域名对应的IP后,一步步向上返回,直到返回给浏览器。
发起TCP请求
浏览器会选择一个大于1024的本机端口向目标IP地址的80端口发起TCP连接请求。经过标准的TCP握手流程,建立TCP连接。
关于TCP协议的细节,这里就不再阐述。这里只是简单地用一张图说明一下TCP的握手过程。如果不了解TCP,可以选择跳过此段,不影响本文其他部分的浏览。
什么是负载均衡?当一台服务器无法支持大量的用户访问时,将用户分摊到两个或多个服务器上的方法叫负载均衡。
什么是Nginx?Nginx是一款面向性能设计的HTTP服务器,相较于Apache、lighttpd具有占有内存少,稳定性高等优势。
负载均衡的方法很多,Nginx负载均衡、LVS-NAT、LVS-DR等。这里,我们以简单的Nginx负载均衡为例。关于负载均衡的多种方法详情大家可以Google一下。
1) 一般,如果我们的平台配备了负载均衡的话,前一步DNS解析获得的IP地址应该是我们Nginx负载均衡服务器的IP地址。所以,我们的浏览器将我们的网页请求发送到了Nginx负载均衡服务器上。
2) Nginx根据我们设定的分配算法和规则,选择一台后端的真实Web服务器,与之建立TCP连接、并转发我们浏览器发出去的网页请求。
Nginx默认支持 RR轮转法 和 ip_hash法 这2种分配算法。
前者会从头到尾一个个轮询所有Web服务器,而后者则对源IP使用hash函数确定应该转发到哪个Web服务器上,也能保证同一个IP的请求能发送到同一个Web服务器上实现会话粘连。
也有其他扩展分配算法,如:
fair:这种算法会选择相应时间最短的Web服务器
url_hash:这种算法会使得相同的url发送到同一个Web服务器
3) Web服务器收到请求,产生响应,并将网页发送给Nginx负载均衡服务器。
4) Nginx负载均衡服务器将网页传递给filters链处理,之后发回给我们的浏览器。
浏览器渲染
1) 浏览器根据页面内容,生成DOM Tree。根据CSS内容,生成CSS Rule Tree(规则树)。调用JS执行引擎执行JS代码。
2) 根据DOM Tree和CSS Rule Tree生成Render Tree(呈现树)
3) 根据Render Tree渲染网页
19、c++异常底层的实现
在具体实现异常捕获时,首先,C++ 异常处理器检查发生异常的位置是否在当前函数的某个 try 块之内。这项工作可以通过将当前函数的 nStep 值依次在 piHandler 指向 tblTryBlocks[] 表的条目中进行范围为 [nBeginStep, nEndStep) 的比对来完成。
其次,如果异常发生的位置在当前函数中的某个 try 块内,则尝试匹配该 tblTryBlocks[] 相应条目中的 tblCatchBlocks[] 表。tblCatchBlocks[] 表中记录了与指定 try 块配套出现的所有 catch 块相关信息,包括这个 catch 块所能捕获的异常类型及其起始地址等信息。
若找到了一个匹配的 catch 块,则复制当前异常对象到此 catch 块,然后跳转到其入口地址执行块内代码。
否则,则说明异常发生位置不在当前函数的 try 块内,或者这个 try 块中没有与当前异常相匹配的 catch 块,此时则沿着函数栈框架中 piPrev 所指地址(即:异常处理链中的上一个节点)逐级重复以上过程,直至找到一个匹配的 catch 块或到达异常处理链的首节点。对于后者,我们称为发生了未捕获的异常,对于 C++ 异常处理器而言,未捕获的异常是一个严重错误,将导致当前进程被强制结束。
在编译一段 C++ 代码时,编译器会将所有 throw 语句替换为其 C++ 运行时库中的某一指定函数,这里我们叫它 __CxxRTThrowExp(与本文提到的所有其它数据结构和属性名一样,在实际应用中它可以是任意名称)。该函数接收一个编译器认可的内部结构(我们叫它 EXCEPTION 结构)。这个结构中包含了待抛出异常对象的起始地址、用于销毁它的析构函数,以及它的 type_info 信息。对于没有启用 RTTI 机制(编译器禁用了 RTTI 机制或没有在类层次结构中使用虚表)的异常类层次结构,可能还要包含其所有基类的 type_info 信息,以便与相应的 catch 块进行匹配。
在图5中的深灰色框图内,我们使用 C++ 伪代码展示了函数 FuncA 中的 “throw myExp(1);” 语句将被编译器最终翻译成的样子。实际上在多数情况下,__CxxRTThrowExp 函数即我们前面曾多次提到的“异常处理器”,异常捕获和栈回退等各项重要工作都由它来完成。
__CxxRTThrowExp 首先接收(并保存)EXCEPTION 对象;然后从 TLS:Current ExpHdl 处找到与当前函数对应的 piHandler、nStep 等异常处理相关数据;并按照前文所述的机制完成异常捕获和栈回退。由此完成了包括“抛出”->“捕获”->“回退”等步骤的整套异常处理机制。 |
20、内存分区
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改
一般认为在c中分为这几个存储区:
1. 栈 --有编译器自动分配释放
2. 堆 -- 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收
3. 全局区(静态区) -- 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束释放。
4. 另外还有一个专门放常量的地方。程序结束释放
21、IO阻塞非阻塞同步异步问题
先说阻塞与非阻塞的区别,recv()函数默认是阻塞的,什么是阻塞呢?就是当你调用recv()函数时,整个进程或者线程就等待在这里了,直到你recv的fd的所有信息都被send过来,这么做好处就是保证所有信息都能够完整的读取了,但劣势也很明显,就是在recv()的过程中你的进程或线程做不了其它事情,由此,引入了非阻塞IO。
非阻塞IO是什么呢,还是以recv()函数为例,当你将其设置为非阻塞时,每次当你recv()时,就直接返回,不管信息有没有完全send进来,好处很明显,recv()了之后进程马上能处理下一行代码,坏处也很明显,就是你不知道你的消息是否读完了,这种问题就是TCP中大名鼎鼎的半包问题(解决办法主要是通过一个buffer缓存所有读进来的消息)
下面再说同步与异步的区别,在POSIX定义中把同步IO操作定义为导致进程阻塞直到IO完成的操作,反之则是异步IO,看概念感觉异步跟非阻塞好像也没有什么区别,要好好理解同步和异步,就要详细说明下IO过程:
1.数据准备阶段
2.内核空间复制回用户进程缓冲区空间
无论阻塞式IO还是非阻塞式IO,都是同步IO模型,区别就在与第一步是否完成后才返回,但第二步都需要当前进程去完成,异步IO呢,就是从第一步开始就返回,直到第二步完成后才会返回一个消息,也就是说,非阻塞能够让你在第一步时去做其它的事情,而真正的异步IO能让你第二步的过程也能去做其它事情。这里就在说一下select,poll和epoll这几个IO复用方式,这时你就会了解它们为什么是同步IO了,以epoll为例,在epoll开发的服务器模型中,epoll_wait()这个函数会阻塞等待就绪的fd,将就绪的fd拷贝到epoll_events集合这个过程中也不能做其它事(虽然这段时间很短,所以epoll配合非阻塞IO是很高效也是很普遍的服务器开发模式--同步非阻塞IO模型)。有人把epoll这种方式叫做同步非阻塞(NIO),因为用户线程需要不停地轮询,自己读取数据,看上去好像只有一个线程在做事情,也有人把这种方式叫做异步非阻塞(AIO),因为毕竟是内核线程负责扫描fd列表,并填充事件链表的,个人认为真正理想的异步非阻塞,应该是内核线程填充事件链表后,主动通知用户线程,或者调用应用程序事先注册的回调函数来处理数据,如果还需要用户线程不停的轮询来获取事件信息,就不是太完美了,所以也有不少人认为epoll是伪AIO,还是有道理的。
22、tcp发送窗口接收窗口拥塞窗口的区别
一:滑动窗口是接受数据端使用的窗口大小,用来告知发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的,对应==>rwnd:接收端窗口(receiver window)
对于流量控制,是一个端对端的概念。由接收端返回的rwnd控制。
二:那么对于数据的发送端就是拥塞窗口了,拥塞窗口不代表缓存,拥塞窗口指某一源端数据流在一个RTT内可以最多发送的数据包数,cwnd:发送端窗口( congestion window )。
拥塞控制: 发送端主动控制控制cwnd,有慢启动(从cwnd初始为1开始启动,指数启动),拥塞避免(到达ssthresh
后,为了避免拥塞开始尝试线性增长),快重传(接收方每收到一个报文段都要回复一个当前最大连续位置的确认,
发送方只要一连收到三个重复确认就知道接收方丢包了,快速重传丢包的报文,并TCP马上把拥塞窗口 cwnd 减小到
1),快恢复(直接从ssthresh线性增长)。
快重传的机制是:
-1. 接收方建立这样的机制,如果一个包丢失,则对后续的包继续发送针对该包的重传请求;
-2. 一旦发送方接收到三个一样的确认,就知道该包之后出现了错误,立刻重传该包;
-3. 此时发送方开始执行“快恢复”算法:
*1. 慢开始门限减半;
*2. cwnd设为慢开始门限减半后的数值;
*3. 执行拥塞避免算法(高起点,线性增长);
三:发送方窗口是相互影响的,具体如下:
发送方窗口的上限值 = Min [ rwnd, cwnd ]
当rwnd < cwnd 时,是接收方的接收能力限制发送方窗口的最大值。
当cwnd < rwnd 时,则是网络的拥塞限制发送方窗口的最大值。
23、数据库索引的实现
https://blog.csdn.net/sdgihshdv/article/details/75039825
24、数据库如何实现数据一致性问题(锁)乐观锁和悲观锁的概念
先说悲观锁和乐观锁吧。并发控制一般采用三种方法,分别是乐观锁和悲观锁以及时间戳。乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据;悲观锁就刚好相反,觉得自己读数据库的时候,别人可能刚好在写自己刚读的数据,其实就是持一种比较保守的态度;时间戳就是不加锁,通过时间戳来控制并发出现的问题。悲观锁就是在读取数据的时候,为了不让别人修改自己读取的数据,就会先对自己读取的数据加锁,只有自己把数据读完了,才允许别人修改那部分数据,或者反过来说,就是自己修改某条数据的时候,不允许别人读取该数据,只有等自己的整个事务提交了,才释放自己加上的锁,才允许其他用户访问那部分数据。乐观锁就比较简单了,就是不做控制,这只是一部分人对于并发所持有的一种态度而已。时间戳就是在数据库表中单独加一列时间戳,比如“TimeStamp”,每次读出来的时候,把该字段也读出来,当写回去的时候,把该字段加1,提交之前 ,跟数据库的该字段比较一次,如果比数据库的值大的话,就允许保存,否则不允许保存,这种处理方法虽然不使用数据库系统提供的锁机制,但是这种方法可以大大提高数据库处理的并发量,因为这种方法可以避免了长事务中的数据库加锁开销(操作员A 和操作员B操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系 统整体性能表现。 需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局 限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户 余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如 将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途 径,而不是将数据库表直接对外公开)。以上悲观锁所说的加“锁”,其实分为几种锁,分别是:排它锁和共享锁,其中排它锁又称为写锁,共享锁又称为读锁。(ps.可以参考此文 “http://blog.sina.com.cn/s/blog_548bd2090100ir7k.html”)
26、范式和反范式
1NF:无重复的列.表中的每一列都是不可分割的基本数据项.不满足1NF的数据库不是关系数据库.
2NF:属性完全依赖于主键.不能存在仅依赖于关键一部分的属性.
3NF:属性不传递依赖于其它非主属性.非主键列必须直接依赖于主键,而不能传递依赖。即不能是:非主键A依赖于非主键B,非主键B依赖于主键.
范式可以避免数据冗余,减少数据库的空间,减轻维护数据完整性的麻烦.但等级越高的范式设计出来的表越多,可能会增加查询所需时间.当我们的业务所涉及的表非常多,经常会有多表连接,并且我们对表的操作要时间上要尽量的快,这时可以考虑我们使用“反范式”.也就是用空间来换取时间,把数据冗余在多个表中,当查询时可以减少或者是避免表之间的关联.
比如两个表(用户id,好友id)和(用户id,用户昵称,用户邮箱,联系电话)符合3NF,如果需查询某个用户的好友(昵称)名单,此时需对2个表进行连接查询,可以把第一个表修改成(用户id,好友id,好友昵称)这样只需要查询第一个表就可获取所有好友昵称.