MQ

消息队列引入的好处 通过异步处理提高系统性能(减少响应所需时间) 削峰/限流 降低系统耦合性。 消息队列引入的问题 系统可用性降低:需要处理mq宕机问题 系统复杂度提高:需要处理消息重复、丢失、保序等问题 一致性问题:消息没有被正确消费的话,引入一致性问题 kafka特点 高吞吐量、低延迟。topic可以分为多个partition,消费者组的消费者并行对topic进行消费 可扩展性:支持热扩展 持久性:消息被持久化到磁盘,并支持数据备份 容错性:partition replica 高并发:支持数千个客户端同时进行读写 kafka架构 Producer(生产者) : 产生消息的一方。 Consumer(消费者) : 消费消息的一方。 Consumer Group(消费者组):同一个组内不同消费者负责消费不同的partation,消费者组之间互不影响 Broker(代理) : 可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。 Controller:通过 zk 从 Brokers 中选举出来管理整个 Broker 集群的 Broker,名为 Controller。Controller 通过定时任务,或者监听器模式获取 zk 信息,将 zk 的变动通过事件的方式发送给事件队列,队列就是一个LinkedBlockingQueue,事件消费者线程组通过消费消费事件,将相应的事件同步到各 Broker 节点。 每个Broker又包含了topic和partition Topic(主题) : Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic 来消费消息。 Partition(分区) : 一个 Topic 划分为 Partition ,分布在不同的 Broker 上,便于负载均衡以及并发消费。每个 Partition 以文件夹的形式存储在文件系统中。每个对应的 Partition 数据目录下存储 .index,.log ,.timeindex三种文件 partition实际上就可以看成是一个队列。 ...

九月 2, 2024 · by NOSAE

MySQL

select流程 连接 获取TCP连接,查询用户的权限,该权限保存在连接中,就算管理员改了用户权限,该连接的权限不会变。 空闲连接的最大空闲时长由wait_time控制,超过最大时长就自动断开。 最大连接数由max_connections控制,超过最大连接数就拒绝新的连接。 MySQL连接也分长连接和短连接: 连接 mysql 服务(TCP 三次握手) 执行sql 断开 mysql 服务(TCP 四次挥手) // 长连接 连接 mysql 服务(TCP 三次握手) 执行sql 执行sql 执行sql .... 断开 mysql 服务(TCP 四次挥手) 由于很多资源保存在内存中,并且在连接断开后才释放,长连接很多很可能MySQL程序占用内存过大导致被系统杀死并重启,对于长连接的解决方式是客户端主动调用mysql_reset_connection重置连接释放内存,这个过程不需要重连和重新做权限验证。 在实际开发中(以Java为例),会使用DBCP、C3P0或者Druid的连接池来管理连接,由连接池处理这种问题。 解析SQL 词法分析+语法分析。 这一步不会检查表/字段是否存在,只是单纯检查SQL语法 执行SQL 预处理:检查表/字段是否存在、将select *展开为所有字段 优化:制定执行计划,选择索引(使用explain SQL语句可以查看执行计划) 执行:执行器根据执行计划,将索引传给引擎去查询一条符合的记录,得到一条记录后检查剩余的非索引条件是否满足,满足的话发送这条记录给客户端。不断重复上面这个过程。 redo log(引擎层) redo log具有crash safe的能力 redo log是物理日志,记录的是“在某个数据页上做了什么修改” redo log是循环写的,空间固定会用完。 redo log buffer是全局共用的,参数innodb_flush_log_at_trx_commit=N控制write和fsync,N=0的时候不write也不fsync,N=1的时候每个事务提交都write+fsync,N=2的时候只write bin log(服务层) binlog是归档日志,没有crash safe的能力 binlog是逻辑日志,比如“给 ID=2 这一行的 c 字段加 1" binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志 binlog+某个时间点的数据库全量备份,可以将数据库恢复到之前某个时间点,比如误删了数据库想要恢复: 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库; 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。 每个线程维护自己的binlog cache,参数sync_binlog=N控制fsync,0为不fsync,N为每N个事务fsync。为N的话,binlog最多会丢失N个binlog日志。 ...

九月 2, 2024 · by NOSAE

OS

软中断 中断请求的处理程序应该要短且快,因为中断处理程序要求关中断(不接受新的中断请求),如果中断处理程序执行时间过长,可能在还未执行完中断处理程序前,会丢失当前其他设备的中断请求。 为了解决由于中断处理时间过长,导致新来的中断丢失,将中断分成两个部分: 硬中断:先关中断,处理跟硬件紧密相关或者时间敏感的事情 软中断:由内核触发,完成该中断剩余的耗时工作 其中硬中断直接抢占cpu,而软中断有专门的内核线程ksoftirqd处理,由操作系统调度执行。 软中断不只是设备中断的下半部分,一些内核自定义事件也属于软中断,比如内核调度等、RCU 锁。 linux上/proc/softirqs记录当前各类型软中断个数,如果NET_RX个数变化过快,说明很多网络包打进来,可以用tcpdump抓包分析,如果发现是异常流量,可以加防火墙,如果是正常流量,需要考虑升级硬件。 进程的状态 挂起态:处于阻塞状态的进程,进程可能会占用着物理内存空间,那么,就需要一个新的状态,来描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态。(sleep或者ctrl+z) 进程和线程比较 创建: 进程是资源(内存、文件等)分配的基本单位,线程是调度的基本单位 进程创建涉及内存管理、文件管理等操作。而线程独享的资源只有寄存器和栈,对于内存和文件只需要共享即可 线程销毁更快,因为相对要释放的资源更少 切换调度 同一进程的线程切换更快,因为线程共享进程的地址空间,内存管理单元不涉及切换过程,不需要切换页表,不用冲刷TLB,要切换的上下文信息也少了很多 数据传递 同一进程的线程传递数据更快,由于共享内存、文件等资源,那么线程传递数据不需要经过内核,经过内核意味着需要切换到系统栈上执行,相当于切换上下文,并且系统调用由于不相信外界用户代码,会有额外的检查工作 死锁 条件: 互斥 资源不可剥夺 循环等待 占有并等待 切换内核态开销大的原因 每个进程都会有两个栈,一个内核态栈和一个用户态栈。当中断执行时就会由用户态栈转向内核态栈,系统调用时需要进行栈的切换,而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。 虚拟内存 进程访问一个虚拟地址时,CPU芯片中的内存管理单元MMU会将其映射为物理地址,最终拿着这个物理地址去访问内存。而虚拟地址物理地址的映射方式主要有分段和分页两种。 分段:由程序员将虚拟内存划分为一个个段,虚拟地址划分为段号和段内偏移量,段号用于在段表中查询段的物理基地址、段大小、特权等级等。段基地址+段内偏移量就构成了物理地址。 分段存在的问题: 内存碎片:会有外部碎片。解决外部碎片是通过内存交换,也就是通过swap的方式实现了碎片整理,具体是将在用的内存先交换到硬盘上,然后再装载到内存当中,但是不是装载回原来的位置,而是紧邻上一片正在使用的内存。 内存交换率低:由于涉及到读写硬盘,因此swap的效率低是必然的。 综上,分段由于经常出现外部内存碎片,经常触发swap整理碎片,导致整个机器的卡顿。 分页:由操作系统将虚拟内存划分成一个个大小相同的页,虚拟地址划分为页号和页内偏移量,页号用于在页表中查询页的物理页号,拼接上页内偏移量就构成了物理地址。 分页的特点:消除了外部碎片,并且通过限制页的大小,限制了每次swap也只有一个或几个页,提高swap的效率。但是由于操作系统给进程分配物理内存是以页为单位的,当一个页没有全部使用到的话,会出现内部碎片,但页大小比较小,因此内部碎片的大小也会控制在一定范围内。 多级页表:由于一个虚拟地址必须通过查询页表来找到对应的物理页,而32位机器+4KB页大小的环境下,一个进程的页表占据4MB大小,这样纯属是一种浪费。因此引入二级页表,让一级页表长居内存,而二级页表在需要的时候再加载到内存,由于程序的空间局部性,最好的情况下内存只需要为进程保留一级页表和一页的二级页表。 linux内存布局 每个进程的虚拟内存空间划分为用户空间和内核空间,所有进程的内核空间实际上映射的是同一块物理内存: 下面是用户空间的具体布局: 其中文件映射段的内存和堆内存一样都是动态分配的,比如使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。 malloc分配内存的原理 malloc可能会使用brk或者mmap这两个系统调用来向操作系统申请内存:当分配内存小于128KB则用brk,大于128KB则用mmap。brk的内存在free之后会归还内存池,不会归还给操作系统,而mmap的内存free之后直接归还给操作系统。因此,如果全部使用mmap来分配内存,那么每次分配内存都触发系统调用,并且每次都触发缺页中断,效率非常低。 OOM原理 触发异步回收,不阻塞进程执行 触发同步回收,阻塞进程执行 OOM 可以被回收的内存分为文件页和匿名页,对于脏页,会触发IO写回硬盘,否则可以直接释放。对于匿名页(堆、栈等用户程序使用到的内存)则通过swap机制换出到硬盘。因为触发磁盘IO,降低系统运行效率,看起来就是发生卡顿。 如果没有空闲文件页和匿名页,并且也没有未分配的物理内存,就会触发OOM,根据算法选择占用物理内存较高的进程,然后将其杀死,以便释放内存资源,直到有足够的内存进行分配。 如何保护一个进程不被OOM杀掉:OOM killer选择被杀程序的时候通过计算一个分数来决定,分数越高越优先被杀 // points 代表打分的结果 // process_pages 代表进程已经使用的物理内存页面数 // oom_score_adj 代表 OOM 校准值 // totalpages 代表系统总的可用页面数 points = process_pages + oom_score_adj*totalpages/1000 oom_score_adj可设置的范围为[-1000, 1000],默认为0,只与进程占用的物理内存页数相关。可以通过设置oom_score_adj为 -1000,降低该进程被 OOM 杀死的概率。一般是特别重要的系统服务才会做这样的配置。 ...

九月 2, 2024 · by NOSAE

Redis

字符串 实现 使用SDS(简单动态字符串),SDS不仅可以保存字符串还可以保存二进制数据,获取长度的时间复杂度是O(1),SDS的API是安全的,比如拼接字符串不会造成缓冲区溢出,总的来说就是对数组封装,提供一系列方便操作的API。 字符串对象有三种编码:int、raw、embstr 整数:ptr从void*转换为long 短字符串(至于多短,每个redis版本不一样):分配一块连续空间保存redisObject和SDS 长字符串:分别为redisObject和SDS分配两个内存,redisObject.ptr指向SDS embstr如果要修改大小的话,只能重新分配空间。因此embstr实际上是只读的,当要修改embstr的长度,redis会先将其转换为raw再进行修改。 使用场景 缓存对象、常规计数(INCR)、分布式锁(SET-NX)、共享Session List 列表 List在使用上就是一个Deque双端队列,存字符串 实现 redis3.2之前:小列表使用压缩列表实现,大列表使用双向链表实现 redis3.2之后:使用quicklist实现 使用场景 消息队列: 保序:LPUSH+RPOP,但是RPOP需要轮询,浪费CPU性能,因此还提供了BRPOP阻塞式读取 处理重复:每条消息设置一个全局唯一ID,利用ID判断是否已经消费,List不会为消息生成ID,需要用户自己添加 保证可靠:BRPOPLPUSH,读取的同时将消息插入另一个List作为留存,如果用户处理消息时失败,下次从留存List重新读取 作为消息队列的缺点: 不支持消费者组 Hash 哈希 适合存储对象 实现 redis7.0之前:小Hash使用压缩列表,大Hash使用哈希表 redis7.0之后:小Hash使用listpack,大Hash使用哈希表 使用场景 缓存对象:一般可以用String+序列化存储对象,并将变化频繁的字段抽出来用Hash存储 Set 集合 实现 元素都是整数的小Set:整数集合 否则:哈希表 使用场景 点赞:保证每个用户只能点一次赞 共同关注(SINTER) 推荐关注(SDIFF) 抽奖(允许重复中奖SRANDMEMBER,不允许SPOP) 潜在风险:「并、交、差」的计算复杂度高,数据量大的情况下会阻塞redis。可以用从库进行计算,或交给客户端来自行处理,从而不阻塞主库 ZSet 有序集合 比Set多了一个score,按照score排序。ZSet不支持「差」运行 实现 redis7.0之前:小zset使用压缩列表,大zset使用跳表 redis7.0之后:小zset使用listpack,大zset使用跳表 使用场景 排行榜:score最大的前几个(ZREVRANGE),范围score内最小的前几个(ZRANGEBYSCORE) 电话排序:获取132、133开头的号码( ZRANGEBYLEX phone [132 (134 ),不要在分数不一致的 SortSet 集合中去使用 ZRANGEBYLEX和 ZREVRANGEBYLEX 指令,因为获取的结果会不准确 BitMap 实现 String 使用场景 签到统计:比如一年的签到只需要365个bit 判断登陆态:用户ID作为offset,如果ID是连续的,5000 万用户只需要 6 MB 的空间(5000万位) ...

九月 2, 2024 · by NOSAE

网络

tcp报文 tcp三次握手 客户端发送:SYN、随机序列号x 服务端发送:SYN、ACK、随机序列号y、确认应答号x+1 客户端发送:ACK,可以携带数据 tcp为什么不是两次握手 防止旧的SYN建立连接:如果只有两次握手,那么服务端收到SYN后直接进入established状态(此时可以发送数据),然后返回ack给客户端,如果这个SYN是旧的,那么最终客户端发现不是想要的ack,就会发送rst断开连接,那么服务端又要去断开已经建立好的连接,浪费资源。 如果是三次握手,那么服务端不会直接进入established。 同步序列号:初始化序列号是最重要的,所以客户端发送初始序列号x(第一次握手),客户端需要得知服务端已经收到并且服务端发送初始序列号y(第二次握手),服务端需要得知客户端已经收到(第三次握手) tcp keepalive keepalive是TCP保鲜定时器,链接空闲时的心跳机制。 当超过一段时间之后,TCP自动发送一个数据为空的报文给对方,如果对方回应了这个报文,说明对方还在线,链接可以继续保持,如果对方没有报文返回,并且重试了多次之后则认为链接丢失,没有必要保持链接。 tcp四次挥手 客户端发送:FIN 服务端发送:ACK 服务端发送:FIN 客户端发送:ACK 客户端进入TIME_WAIT,等待2MSL(报文最大生存时间) 其中,客户端一直收不到第三次握手FIN的话,客户端有两种情况: 对于客户端调用shutdown()的情况,只关闭发送数据不关闭接收数据,因此客户端死等 对于客户端调用close()的情况,同时关闭发送和接收数据,长时间收不到FIN就会主动close 服务端一直收不到第四次握手ACK的话(在这之前处于CLOSED_WAIT,并且服务端调用close(),发送了FIN),就会主动close。 tcp四次挥手客户端为什么要TIME_WAIT 原因: 等待历史连接的数据都已经在网络中自然消亡:如果没有TIME_WAIT,假设此时客户端建立新的连接,并收到了上个连接中延迟到达的报文,并且序列号恰好在客户端的滑动窗口内,那么则接收到了错误的数据。 保证服务端能正确关闭:等待足够的时间让ACK发到对面,如果由于网络原因服务端收不到的话就会重发FIN,客户端收到后重置计时器为2MSL,重传ACK。如果没用TIME_WAIT,客户端收到重传FIN的时候就会回一个RST,虽然服务端也能关闭,但是是将其解释为错误,可能会使用户迷惑。 滑动窗口 滑动窗口用于提高发送数据的速率以及流量控制,每个窗口的单位为1个MSS大小的数据(一个 TCP 报文的最大长度,为了避免超过MTU造成分片,因为丢失一个分片就得重传整个tcp报文) 滑动窗口就是一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。这样一来,就不用发一个数据就等一个ack,可以把窗口的数据连续发送了。 累计确认:ack=n表示序号为n之前的报文都收到了,就算之前的ack都丢失也没关系。 窗口大小:窗口的大小由接收方的窗口大小来决定,由接收方告诉自己还有多少缓冲区可以接收数据,即发送端窗口不能大于接收端窗口 tcp Nagle发送算法 解决发送数据量太小,头部占比很大,性价比很低(即糊涂窗口综合症)。伪代码如下: if 有数据要发送 { if 可用窗口大小 >= MSS and 可发送的数据 >= MSS { 立刻发送MSS大小的数据 } else { if 有未确认的数据 { 将数据放入缓存等待接收ACK } else { 立刻发送数据 } } } 根据代码,为了避免糊涂窗口综合症,需要:接收方「小窗口直接告诉发送方窗口为0」+ 发送方开启 Nagle 算法 拥塞控制 为了有了流量控制后,还需要拥塞控制?只需要考虑最极端的情况,如果发送方和接收方的传输和接收能力都是无限的,那么瓶颈就出现在网络中,如果无限制地发送,网络只会越来越拥塞,因此需要拥塞控制。 拥塞控制也基于滑动窗口,并加入「拥塞窗口」的概念,因此发送方窗口=min(接收方窗口,拥塞窗口)。 拥塞窗口如何增长: 慢启动:每收到一个ACK,拥塞窗口+1。拥塞窗口初始为1,第一次收到ACK,1+1=2,发送两个包,收到两个ACK,2+2=4, 8, 16…慢启动每轮发送是指数增长的。 拥塞避免:当慢启动超过阈值,每收到一个ACK,拥塞窗口+1/cwnd,总的来看就是每轮发送才+1,而不是每收到一个ACK+1.拥塞避免每轮发送是线性增长的。 拥塞窗口如何收缩(发生拥塞后): 拥塞发生:发生超时重传的时候,慢启动阈值设为cwnd/2,cwnd设为1 快速回复:发生快速重传的时候,慢启动阈值设为cwnd/2,cwnd设为慢启动阈值 tcp粘包解决 特殊字符作为消息结束符 自定义消息结构,比如在头部定义一个消息长度 tcp已经处于established服务端收到新的SYN 服务端会回复属于它的连接的ack,这样客户端发现不是自己想要的ack,就会回一个rst,然后服务端就会释放这个连接 ...

九月 2, 2024 · by NOSAE