Linux高性能服务器基础
学习过程简单记录一些知识点和自己的疑问参考《[Linux 高性能服务器编程].2013.中文版》、小林coding《图解操作系统》虚拟机环境配置现有的是wsl2不是VMware上的完整虚拟机–不在工位电脑配置了需要换笔记本去配置lsb_release -a 检查ubuntu版本uname -r查看内核版本g --version g版本cmake --version 查看cmake是否安装sudo apt updata(检测可以软件包最新版本)因为在root下apt install -y build-essential cmake gitlinux命令Linux 常用操作命令大全最后更新时间2024年1月_linux常用命令-CSDN博客pwd# 显示当前路径/#根目录ls# 查看当前目录下的所有目录和文件ls/#查看目标目录下的所有目录和文件cd# 切目录cd..#返回上一级目录mkdir# 创建文件夹rm# 删除rm-rf文件夹名#删除文件夹cp# 复制mv# 移动cat文件名# 查看文件内容less文件名#分页查看文件内容适合长文件进去后空格翻页q退出grep关键字文件名#在文件内搜索关键字find# 找文件sudo# 管理员权限tree-L2#按树状看目录结构-L 2表示只看两层pwdlscd~mkdirtest_linux#创建文件夹cdtest_linux#切换到这个目录下toucha.txt#创建a.txt,文件存在的话touch会更新文件的最后修改时间ls#列出当前目录Linux目录结构1.TCP/IP协议族数据链路层实现网卡接口的网络驱动程序以处理数据在物理媒介(比如以太网、令牌环等)。不同的物理网络具有不同的电气特性网络驱动程序隐藏这些细节给上层协议提供一个统一的接口。数据链路层常用的协议ARP协议(地址解析协议)和RARP协议(逆地址解析协议)实现了IP地址网络层)和机器物理地址(MAC地址以太网、令牌环和802.11无线网络这种物理媒介都使用MAC地址)之间的互相转换网络层使用IP地址寻址一台机器数据链路层使用物理地址寻址一台机器所以网络层需要先将目标机器的IP地址转化为其物理地址才能使用数据链路层提供的服务(ARP协议)。网络层实现数据包的选路和转发。隐层网络拓扑的细节使得传输层和网络应用程序看来通信双方是直接相连的。广域网WAN通常使用众多分级的路由器来连接分散的主机或局域网LAN。因此通信的两台主机是通过多个中间节点(路由器)连接的。网络层的任务就是选择这些中间节点以确定两台主机之间的通信路径IP协议根据数据报的目的IP地址来决定如何投递使用逐跳(hop by hop)方式确定通信路径。ICMP协议因特网控制报文协议主要用于检测网络连接。传输层为主机上的应用程序提供端到端的通信/TCP/UDP/SCTP协议应用层在用户空间实现前面三层都负责处理网络通信细节需要稳定高效所以在内核控件实现。应用层负责处理文件传输名称查询网络管理等可以减轻内核压力。帧才是最终在物理网络上传送的字节序列网络通信TCP三次握手(建立连接)、四次挥手TCP协议为应用层提供可靠的保证网络包的交付、网络包的按序交付、网络包中数据的完整保证接收端接接收的网络包是无损坏、无间隔、非冗余和按序的、面向连接的一对一、基于字节流TCP报文是有序的的服务使用TCP协议通信的双方必须先建立TCP连接并在内核中为该连接维持一些必要的数据结构比如连接的状态、读写缓冲区以及诸多定时器等。当通信结束双方必须关闭连接以释放这些内核数据。三次握手-TCP建立连接TCP头部格式的控制位SYN 1 希望建立连接并初始化序列号ACK 1 确认应答号生效按序进行传输FIN 1 传输结束断开连接RST 1 连接异常强制断开第一次握手第一个报文 SYN报文SYN1,ACK0;服务端主动监听某个端口处于LISTEN状态客户端初始化序列号(client_isn)将此序列号置于TCP头部的序列号字段中同时把SYN标志位置为1将第一个SYN报文发给服务端表示向服务端发起连接该报文不包含应用层数据之后客户端处于SYN-SENT状态。第二次握手第二个报文 SYNACK报文SYN1,ACK1;服务端收到客户端的 SYN 报文后首先服务端也随机初始化自己的序号server_isn将此序号填入 TCP 首部的「序号」字段中其次把 TCP 首部的「确认应答号」字段填入 client_isn 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端该报文也不包含应用层数据之后服务端处于 SYN-RCVD 状态。第三次握手第三个报文 ACK报文SYN0,ACK1客户端收到服务端报文后还要向服务端回应最后一个应答报文首先该应答报文 TCP 首部 ACK 标志位置为 1 其次「确认应答号」字段填入 server_isn 1 最后把报文发送给服务端这次报文可以携带客户到服务端的数据之后客户端处于 ESTABLISHED 状态。服务端收到客户端的应答报文后也进入ESTABLISHED状态。第三次握手可以携带数据前两次不可以完成三次握手客户端和服务端均处于ESTABLISHED状态可以互相发送数据。三次握手才可以初始化Socket、序列号和窗口大小并建立TCP连接1.为了防止旧的重复连接初始化造成混乱。重传的SYN的序列号[Seq Num]应该是一样的。2.同步双方初始序列号。3.避免浪费资源(即两次握手会造成消息滞留情况下服务端重复接受无用的连接请求SYN报文而造成重复分配资源。建立连接的初始序列号不同2.socket-Linux网络编程基础API-网络通信中应用层与传输层之间的编程接口(API),封装了TCP/IP协议栈的细节socket地址API主机字节序小端字节序整数的高位字节储存在内存的高地址处低位字节储存在内存的低地址处网络字节序大端字节序整数的高位字节存储在内存的低地址处格式化的数据通过网络传输时都应该使用这些函数转换字节序#includenetinet/in.h unsigned long int htonl(unsigned long int hostlong);//将长整型(32bit)-IP 的主机host字节序数转化为网络字节序数 unsigned short int htons(unsigned short int hostshort); unsigned long int ntonl(unsigned long int netlong); unsigned short int ntons(unsigned short int netshort);socket一个IP地址和端口对(ip,port)-狭义广义-文件描述符// 用这个结构体 struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(8080); // 端口 inet_pton(AF_INET, 127.0.0.1, addr.sin_addr); // inet_pton IP字符串转二进制 P92 ,后面指定要连接的服务器地址创建socketsocket命名-将一个socket和socket地址绑定-bind服务器程序通常需要命名socket只有命名后客户端才知道该如何连接它。客户端一般采用匿名方式即使用操作系统自动分配的socket地址。监听socket-创建监听队列以存放待处理的客户连接被监听的socket是服务器端接收连接-accept服务器端接受连接发起连接-connect-客户端发起连接3.epoll,事件管理器在Linux中一切皆文件socket是文件描述符也是事件源每个socket可以关联两种事件可读事件可写事件一次只能处于“就绪”或“未就绪”两种状态之一。Linux一切皆文件创建epoll文件描述符-epoll_creat()epoll需要使用一个额外的文件描述符来唯一标识内核中这个事件表创建这个文件描述符用epoll_create()#includesys/epoll int epoll_create(int size)操作epoll的内核事件表-epoll_ctl-写入、修改。删除使用epoll_ctl来操作epoll的内核事件表#includesys/epoll.h int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event) /*fd参数是要操作的文件描述符op参数指定操作类型有以下三种 EPOLL_CTL_ADD往事件表中注册fd上的事件 EPOLL_CTL_MOD修改fd上的注册事件 EPOLL_CTL_DEL删除fd上的注册事件 event指定事件是epoll_event结构指针类型event-data/events.如果是结构体类型 struct epoll_event ev,则里面包含ev.events/data.epoll_wait()-epoll系列系统调用接口函数该函数在一段超时时间内等待一组文件描述符上的事情# int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);4.线程池、进程池两者类似进程池是由服务器预先创建的一组子进程池中的子进程2都运行相同的代码并具备相同的属性比如优先级、PGID等。当新的任务到来时主进程会通过某种方式(主动选择算法/共享工作队列唤醒)选择进程池中的某一个子进程来为之服务。之后使用某种通知机制告诉目标子进程有新任务需要处理并传递必要的数据。并发模式半同步/半反应堆模式-由主进程统一管理监听socket和连接socket半同步/半异步模式和领导者/追随者模式-是由主进程管理所有监听socket各个子进程分别管理属于自己的连接socket。连接就是TCP/UDP连接15.2 处理多客户常连接一个客户的多次请求可以复用一个TCP连接。客户任务无状态(请求之间无依赖)服务器在处理来自同一个客户的不同请求时不需要依赖该客户之前请求所留下的任何上下文或数据。(每个请求独立不保留历史状态任意顺序处理)在15-2中因为两个请求之间没有依赖关系子进程1 处理请求1后产生的任何临时数据不会被子进程2处理请求2时需要用到这样可以实现并行处理提高吞吐量但如果客户任务是存在上下文关系请求有依赖则最好一直用同一个子进程来为之服务避免在各个子进程之间传递上下文数据。epoll的EPOLLONESHOT事件能够确保一个客户连接在整个生命周期中仅被一个线程处理epoll 是一种 I/O 多路复用机制负责高效地检测哪些文件描述符上发生了 I/O 事件而进程/线程是执行单元负责实际处理这些事件gRPC-高性能的RPC框架RPC框架-封装好的远程调用框架客户端不需要知道调用细节使用封装好的接口调用存在于远程计算机上的某个对象获取到远程服务器的信息。-一种通过网络从远程计算机程序上请求服务而不需要了解底层网络技术的协议1.为什么用gRPC而不是HTTP答性能更好基于HTTP/2天生支持双向流适合服务间通信支持多语言通信协议基于标准的 HTTP/2 设计支持双向流、二进制框架和消息头压缩、单 TCP 的多路复用特性这些特性使得 gRPC 在发送和接收方面都非常紧凑和高效序列化默认使用protobufProtobuf是一种高效的二进制消息格式序列化的速度很快基于 HTTP/2 PB,实现高性能。超时取消机制。gRPC允许客户端指定他们愿意等待多长时间完成一个RPC。服务器可以决定在超时后取消。防止资源浪费。2.gRPC和你自己写socket有什么区别答gRPC封装了底层网络自动处理连接、序列化、负载均衡只需要定义proto文件和实现业务逻辑3. .proto文件怎么定义服务.proto文件定义对应数据的格式和gRPC服务以Protobuf3proto3 语法为例分三块语法声明 → 定义消息结构体 → 定义 Service 服务 接口方法先写syntax proto3;必须放第一行message定义请求 / 响应数据结构service里用rpc 方法名(入参) returns (出参)定义接口加stream就是流通信不加就是普通单次调用写完这个.proto文件后gRPC 工具会自动生成服务端接口骨架代码客户端 Stub 调用代码4.stub是什么RPC使用代理模式(stub)来实现透明化远程服务调用客户端调用本地代理对象代理内部封装了序列化、网络通信和服务发现等细节使远程调用在接口和行为上与本地调用一致5.同步调用和异步调用的区别同步调用方发起 RPC / 网络请求 →阻塞当前线程→ 卡死等待响应 → 响应回来才往下执行代码。异步发起请求 →立刻返回不阻塞线程→ 线程继续跑其他业务 → 后台等网络响应 → 触发回调函数处理结果。1.内核模块mmap读取高频数据内核模块mmap采集高频数据内核维护的数据本身变化频率高比如CPU软中断、cpu使用情况等采集逻辑是通过自定义实现内核模块直接采集内核内部的详细数据通过mmap机制将内核内存映射到用户空间根据监控需求定义合适的结构并在内核分配一块内存存放在采集到的内核里的各系用指标的原始数据该内存作为内核和用户空间共享的数据区在通过定时器实现每秒定时获取接下来要暴露到用户进程就要将内核模块注册为字符设备并实现mmap回调这样用户空间可以通过mmap将内核分配的内存区映射到用户空空后面首次访问该虚拟地址时操作系统通过缺页异常将这段虚拟地址和内核的物理内存建立映射关系页表项之后无需再次拷贝用户就可以直接访问映射区内存实时获取到最新更新的内核数据用户控件访问的就是内核分配的真是物理内存的映射。实现用户与内核的高效实时交互。2.eBPF(扩展的伯克利包过滤器-内核可编程技术允许用户在不修改内核源码的情况下扩展内核功能广泛应用于网络监控、性能分析和安全控制等领域。1.可以在内核中添加检测点来观察应用程序和内核之间的交互。eBPF 程序是一种非常高效的添加检测点的方法。在加载并进行 JIT 编译JIT-compiled后第 3 章中将看到程序将以原生机器指令在 CPU 上运行。此外在处理事件时无需在内核和用户空间之间进行转换这是一项代价高昂的操作。2.通过eBPF快速创建新的内核功能不同于直接插入内核模块这种会造成不安全问题的行为如果想向内核添加新功能eBPF提供eBPF验证器确保只有在安全运行的情况下加载eBPF程序不会导致机器崩溃或者陷入死循环也不会允许数据被泄露。3.动态加载eBPF 程序可以动态地加载到内核中和从内核中移除。一旦它们被附加到某个事件上无论是什么原因触发该事件它们都会被触发。-可以立即获得对机器上所有活动的可见性。–对云原生部署的影响4.对于性能追踪和安全可观察性eBPF 的另一个优势是相关事件可以在内核中被过滤掉然后再将其发送到用户空间从而减少了开销。毕竟过滤特定的网络数据包正是最初 BPF 实现的目的。如今eBPF 程序可以收集关于系统中各种事件的信息并使用复杂的、定制的可编程过滤器只将相关信息的子集发送到用户空间以下参考https://mp.weixin.qq.com/s/0UdR8PdVCyNVx5rTYFrvYw?search_click_id257613890346813743-1779174653932-6432284752Linux网络接收数据包流程在Linux内核中当数据包到达网卡的时候通过DMA方式将数据映射到内存。然后硬中断通知CPU有数据到来调用硬中断处理函数之后交给软中断去处理。通过ksoftirq调用软中断处理函数收包的软中断处理函数是net_rx_action函数主要将Ring Buffer缓冲区数据做成sk_buff送给上层协议栈进行处理之后数据包经过层层解包最终将数据放入套接字缓冲区中CPU通过将数据拷贝给应用程序。外部机器发送数据通过网络到达接收机器的网卡(物理网卡NIC-二进制数据)网卡通过DMA(Direct Memory Access)方式将数据映射到内存 直接写入内存的Ring Buffer中同时收到数据后网卡发送硬中断IRQ通知CPUCPU简单确认并通知后续处理;Linux触发软中断软中断[NET_RX_SOFTIRQ]由ksoftirqd或者当前CPU去执行软中断处理函数【net_rx_action()】开始收包进入网络协议栈处理net_rx_action会把Ring buffer数据封装成sk_buff(Linux网络包对象)然后协议栈层层解包[Ethernet-IP-TCP/UDP/socket]最后TCP找到对应socket并放入数据。等待应用程序读取数据2.net_ebpf_monitor.cpp eBPF采集网络数据(1)加载eBPF程序到内核skel net_stats_bpf__open(); net_stats_bpf__load(skel); // 加载进内核(2)挂载到网卡的TC钩子(这里是不是体现eBPF的动态加载)bpf_tc_attach(hook, opts_in); // 挂载到网卡入口 bpf_tc_attach(hook, opts_eg); // 挂载到网卡出口每个网络包经过网卡时eBPF程序在内核里直接统计字节数不需要切换到用户态。(3用户态从BPF map[BPF map共享空间可以同时被内核和用户访问]读数据bpf_map_lookup_elem(map_fd_, next_key, stats);内核态到用户态的数据传递拿到已经统计好的结果spdlog日志cout 主要用于调试输出只能简单打印信息缺少日志级别、时间戳、文件持久化等功能。spdlog 是专门的日志库支持 info、warn、error 等日志级别可以自动记录时间、线程信息并支持日志轮转和异步写入。在 Linux 性能监控项目中使用 spdlog 记录指标采集、gRPC 推送以及 MySQL 写入过程中的运行状态和异常信息方便后续排查问题。【C】spdlog光速入门Clogger最简单最快的库 - 缙云烧饼 - 博客园spdlog中每个logger(日志对象)包含一个vector该vector由一个或者多个智能指针shared_ptr组成logger的每条日志都会调用sink对象由sink对象按照formatter(格式化对象spdlog有默认的格式如果有个性化需求可以自定义)的格式输出到sink指定的地方(有可能是控制台、文件等)。trace 最详细调试信息 debug 调试信息 info 正常运行信息 warn 警告 error 错误 critical 严重错误formatterformatter也即格式化对象用于控制日志的输出格式,spdlog自带了默认的formatter一般情况下我们无需任何修改直接使用即可。注意每个sink会有一个formatter默认formatter默认formatter的格式为[日期时间] [logger名] [log级别] log内容Copy[2022-10-13 17:00:55.795] [service] [debug] found env XXXXXXX : true [2022-10-13 17:00:55.795] [func_config] [debug] kafka_brokers : localhost:9092 [2022-10-13 17:00:55.795] [func_config] [debug] kafka_main_topic : kafka_test [2022-10-13 17:00:55.795] [func_config] [debug] kafka_partition_value : -1 [2022-10-13 17:00:55.795] [service] [info] initialized自定义formatter如果默认的formatter不符合需求可以自定义formatter具体方式如下set_parrtern(pattern_string);例如全局级别的spdlog::set_pattern( [%H:%M:%S %z] [thread %t] %v );单个logger级别的some_logger-set_parttern(“ %H:%M:%S %z %v ”);单个sink级别的some_sink- set_parttern(“… %H: %M …”);其中用到了%H %M这些占位符事实上它们都是预先设定好的想要查看所有的占位符情况可以参考以下网站https://spdlog.docsforge.com/v1.x/3.custom-formatting/#pattern-flagssink#每个sink对应着一个输出目标和输出格式它内部包含一个formatter输出目标可以是控制台、文件等地方。所有的sink都在命名空间spdlog::sinks下可以自行探索控制台sink#spdlog中创建控制台sink非常简单该方式创建的sink会输出到命令行终端且是彩色的也可以选非彩色的但是有彩色的应该都会选彩色的吧……。后缀的_mt代表多线程_st代表单线程Copyauto sink1std::make_sharedspdlog::sinks::stdout_color_sink_mt();文件sink#文件sink的类型有很多这里展示几种经典类型Copyauto sink1std::make_sharedspdlog::sinks::basic_file_sink_mt(log_file_name);//最简单的文件sink只需要指定文件名auto sink2std::make_sharedspdlog::sinks::daily_file_sink_mt(log_file_name,path,14,22);//每天的14点22分在path下创建新的文件auto sink3std::make_sharedspdlog::sinks::rotating_file_sink_mt(log_file_name,1024*1024*10,100,false);//轮转文件一个文件满了会写到下一个文件第二个参数是单文件大小上限第三个参数是文件数量最大值其他sink#ostream_sinksyslog_sink…也可以通过继承base_sink创建子类来自定义sink具体可以参考https://spdlog.docsforge.com/v1.x/4.sinks/#implementing-your-own-sinksink的flush问题创建好sink后建议设置flush方式否则可能无法立刻在file中看到logger的内容以下为两种重要的flush方式设置直接设置全局Copyspdlog::flush_every(std::chrono::seconds(1));spdlog::flush_on(spdlog::level::debug);logger#日志对象每个logger内包含了一个vector用于存放sink每个sink都是相互独立因此一个日志对象在输出日志时可以同时输出到控制台和文件等位置使用默认logger#如果整个项目中只需要一个loggerspdlog提供了最为便捷的默认logger注意该logger在全局公用输出到控制台、多线程、彩色Copy//Use the default logger (stdout, multi-threaded, colored)spdlog::info(Hello, {}!,World);创建特定的logger#大部分情况下默认logger是不够用的因为我们可能需要做不同项目模块各自的logger可能需要logger输出到文件进行持久化所以创建logger是很重要的一件事。好在创建logger也是非常简单的方式一直接创建#与创建sink类似我们可以非常便捷的创建logger由于大部分时候一个logger只会有一个sink所以spdlog提供了创建logger的接口并封装了创建sink的过程Copyauto consolespdlog::stdout_color_mt(some_unique_name);//一个输出到控制台的彩色多线程logger可以指定名字autofile_loggerspdlog::rotating_logger_mt(file_logger,logs/mylogfile,1048576*5,3);//一个输出到指定文件的轮转文件logger后面的参数指定了文件的信息方式二组合sinks方式创建#有时候单sink的logger不够用那么可以先创建sink的vector然后使用sinks_vector创建logger以下样例中首先创建了sink的vector然后创建了两个sink并放入vector最后使用该vector创建了logger其中set_level的过程不是必须的register_logger一般是必须的否则只能在创建logger的地方使用该logger关于register的问题可以往下看Copystd::vectorspdlog::sink_ptrsinks;auto sink1std::make_sharedspdlog::sinks::stdout_color_sink_mt();sink1-set_level(MyLoggers::getGlobalLevel());sinks.push_back(sink1);auto sink2std::make_sharedspdlog::sinks::rotating_file_sink_mt(log_file_name,1024*1024*10,100,false);sink2-set_level(spdlog::level::debug);sinks.push_back(sink2);auto loggerstd::make_sharedspdlog::logger(logger_name,begin(sinks),end(sinks));logger-set_level(spdlog::level::debug);spdlog::register_logger(logger);