news 2026/10/13 5:49:24

Keepalived热备、LVS与HAproxy高可用架构选型与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Keepalived热备、LVS与HAproxy高可用架构选型与实战

1. 从单点故障说起:为什么高可用架构不是可选项

很多团队在项目初期都经历过这样的阶段:一台服务器扛下所有流量,跑得好好的,直到某天凌晨磁盘满了、进程崩了、机房网络抖了一下,业务直接归零。这种“单点故障”带来的痛,只有真正被电话叫醒过的人才能体会。高可用(High Availability,简称HA)要解决的核心问题只有一个:当某个节点不可用时,服务能自动切换到备用节点,让用户几乎无感知。

围绕这个目标,业界沉淀出了几条主流技术路线。标题里提到的三个关键词——Keepalived热备、Keepalived+LVS、HAproxy服务器——其实代表了三种不同层次、不同侧重点的高可用方案。它们不是互相替代的关系,而是经常组合使用,分别解决“谁来做主”“流量怎么分发”“请求怎么转发”这三个问题。

Keepalived热备解决的是节点级冗余:两台机器一主一备,主挂了备顶上,靠的是VRRP协议和虚拟IP漂移。Keepalived+LVS解决的是四层负载均衡下的高可用:LVS负责把流量分给后端真实服务器,Keepalived负责让LVS调度器本身不成为单点。HAproxy则工作在七层,能基于HTTP头部、URL、Cookie做更精细的转发和健康检查,同样可以用Keepalived做高可用。

这篇文章适合谁看?如果你正在搭建一套需要7×24小时稳定运行的服务,或者你已经在用Nginx做反向代理但发现它扛不住某些场景,又或者你只是想把“高可用”这三个字从概念变成能落地的配置,那接下来的内容会一步步把原理、选型、配置和踩坑经验讲清楚。我会尽量用生活化的类比解释协议细节,同时给出可以直接抄的配置片段和排查命令。

2. Keepalived热备:VRRP协议与虚拟IP漂移的底层逻辑

2.1 VRRP到底在干什么:一个“虚拟路由器”的选举游戏

Keepalived的核心是VRRP(Virtual Router Redundancy Protocol,虚拟路由器冗余协议)。你可以把它想象成一个小区里只有一部对外公开的“总机号码”,但背后其实有两部电话机。平时由主电话机接听,主电话机坏了,备用电话机立刻接管这个号码,外面打电话的人根本不知道换了机器。

这个“总机号码”就是虚拟IP(VIP)。VRRP把一组服务器组成一个虚拟路由器,这个虚拟路由器有一个VIP和一个虚拟MAC地址。组内的服务器通过优先级(priority)选举出Master,Master持有VIP并响应ARP请求,Backup则处于监听状态。Master定期发送VRRP通告报文(默认1秒一次),Backup如果在3倍通告间隔内没收到通告,就认为Master挂了,触发新一轮选举。

这里有几个关键参数必须理解透:

  • virtual_router_id:同一个VRRP组内必须一致,不同组必须不同,范围1-255。如果两台不相关的Keepalived配了相同的router_id且在同一网段,会互相抢VIP,这是新手最容易踩的坑之一。
  • priority:优先级,数值越大越优先,范围1-254。Master通常配100以上,Backup配100以下。注意255是保留给IP地址拥有者的。
  • advert_int:通告间隔,默认1秒。调小能加快故障检测,但会增加网络开销;调大则相反。
  • state:初始状态,MASTER或BACKUP。但实际角色由优先级和选举决定,state只是启动时的初始声明。
  • authentication:认证方式,推荐用PASS,同一组内密码必须一致。

2.2 抢占模式与非抢占模式:什么时候该让Master“抢回来”

Keepalived默认是抢占模式(preempt):当原来的Master恢复后,如果它的优先级仍然最高,它会重新抢回VIP。这在某些场景下是好事,比如主节点性能更强;但在另一些场景下是灾难,比如主节点刚重启,服务还没完全就绪就抢回流量,导致请求失败。

非抢占模式通过nopreempt参数开启,但有个硬性条件:初始state必须设为BACKUP,且优先级可以比另一台高。这样即使高优先级节点恢复,也不会主动抢回VIP,直到当前Master主动释放。我个人的经验是,数据库主库、有状态服务、启动慢的应用,一律用非抢占;无状态的前端服务可以用抢占,让性能好的机器多扛流量。

2.3 一个能直接用的Keepalived热备配置

假设有两台服务器,IP分别是192.168.1.10和192.168.1.11,VIP是192.168.1.100。主节点配置:

# /etc/keepalived/keepalived.conf global_defs { router_id node_master enable_script_security script_user root } vrrp_script chk_nginx { script "/usr/bin/killall -0 nginx" interval 2 weight -20 fall 2 rise 2 } vrrp_instance VI_1 { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass MyPass123 } virtual_ipaddress { 192.168.1.100/24 dev eth0 label eth0:1 } track_script { chk_nginx } notify_master "/etc/keepalived/notify.sh master" notify_backup "/etc/keepalived/notify.sh backup" notify_fault "/etc/keepalived/notify.sh fault" }

备节点只需改三处:router_id改成 node_backup,state改成 BACKUP,priority改成 90。virtual_router_id、auth_pass、virtual_ipaddress必须完全一致。

vrrp_script这段是精髓:它每2秒检查一次nginx进程是否存在,如果连续2次检查失败(fall 2),就把优先级减20(weight -20)。主节点优先级从100降到80,低于备节点的90,于是触发切换。这比单纯靠VRRP心跳检测更可靠,因为有时候机器没宕机,但服务已经挂了。

注意:killall -0 nginx只是检查进程是否存在,不检查服务是否真正可用。更严谨的做法是用curl -sf http://127.0.0.1/health检查健康接口。

2.4 脑裂:最危险也最容易被忽视的问题

脑裂(split-brain)是指两台节点都认为自己是Master,同时持有VIP。结果就是VIP在两个MAC地址之间来回漂移,或者ARP表混乱,流量时通时断。造成脑裂的常见原因有三个:心跳线断了但服务器还活着、防火墙拦截了VRRP组播、virtual_router_id冲突。

排查脑裂的第一步是看日志:journalctl -u keepalived -f或/var/log/messages。如果看到两台都在发“Entering MASTER STATE”,基本可以确认。解决方案包括:用独立的心跳线(不要和业务网卡混用)、在交换机上放行VRRP协议(协议号112)、配置unicast_peer指定单播心跳而不是依赖组播。

我踩过最坑的一次是云环境里安全组默认拦截了组播,Keepalived根本收不到对方通告,两台都成了Master。后来改成单播模式才解决:

unicast_src_ip 192.168.1.10 unicast_peer { 192.168.1.11 }

3. Keepalived+LVS:四层负载均衡的高可用组合拳

3.1 LVS的三种转发模式:NAT、DR、TUN怎么选

LVS(Linux Virtual Server)是内核级的四层负载均衡器,性能极高,因为它在内核空间处理数据包,不需要把请求交给用户态进程。它有三种工作模式:

模式原理优点缺点适用场景
NAT调度器改写目标IP,请求和响应都经过调度器配置简单,后端可跨网段调度器成为带宽瓶颈小规模、后端数量少
DR(直接路由)调度器只改MAC地址,响应由后端直接回给客户端性能最高,调度器只处理入站后端必须和调度器同网段,需配置VIP在lo上大多数生产环境首选
TUN(隧道)用IP隧道封装请求转发给后端后端可跨机房需要支持隧道协议,配置复杂跨地域分发

生产环境里DR模式用得最多。它的巧妙之处在于:调度器收到请求后,只把目标MAC改成某台真实服务器的MAC,IP层完全不动。真实服务器收到包后,发现目标IP是自己的VIP(配置在lo接口上),就处理请求,然后直接把响应发给客户端,不再经过调度器。这样调度器只承担入站流量,出站流量由后端自己扛,吞吐量大幅提升。

3.2 为什么LVS需要Keepalived:调度器本身不能是单点

LVS再强,如果只有一台调度器,它挂了整个集群就没了。所以标准做法是两台LVS调度器 + Keepalived做高可用。两台调度器组成VRRP组,同时持有VIP,只有Master对外服务。后端真实服务器通过ipvsadm配置在调度器上。

这里有个细节:后端服务器的VIP配置。在DR模式下,每台真实服务器都需要在lo接口上配置VIP,但必须关闭ARP响应,否则它们会响应客户端对VIP的ARP请求,导致流量绕过调度器直接打到后端。配置方法:

# 在每台真实服务器上执行 echo 1 > /proc/sys/net/ipv4/conf/lo/arp_ignore echo 2 > /proc/sys/net/ipv4/conf/lo/arp_announce echo 1 > /proc/sys/net/ipv4/conf/all/arp_ignore echo 2 > /proc/sys/net/ipv4/conf/all/arp_announce ip addr add 192.168.1.100/32 dev lo label lo:0

arp_ignore=1表示只回答目标IP是本机物理接口IP的ARP请求;arp_announce=2表示总是使用最佳本地地址发送ARP。这两个参数配合,确保后端不会“抢答”VIP的ARP。

3.3 调度算法:轮询、加权、最少连接的实际效果差异

LVS支持十几种调度算法,常用的有:

  • rr(轮询):请求依次分给每台后端,适合后端性能一致的场景。
  • wrr(加权轮询):按权重分配,性能强的机器权重高。权重需要根据CPU、内存、实际压测结果来定,不能拍脑袋。
  • lc(最少连接):把请求发给当前连接数最少的后端,适合请求处理时间差异大的场景。
  • wlc(加权最少连接):默认算法,综合了权重和连接数,大多数场景下表现最稳。
  • sh(源地址哈希):同一客户端IP总是打到同一后端,适合需要会话保持的场景,但后端故障时会话会丢失。

我的一般建议是:无状态服务用wlc,有状态但能容忍会话丢失的用wlc,必须会话保持的用sh并配合后端会话共享。不要盲目用rr,因为实际后端性能往往不一致,rr会导致慢的机器堆积请求。

3.4 完整配置:Keepalived+LVS DR模式实战

主调度器Keepalived配置:

virtual_server 192.168.1.100 80 { delay_loop 6 lb_algo wlc lb_kind DR persistence_timeout 50 protocol TCP real_server 192.168.1.20 80 { weight 100 TCP_CHECK { connect_timeout 3 nb_get_retry 3 delay_before_retry 3 connect_port 80 } } real_server 192.168.1.21 80 { weight 100 TCP_CHECK { connect_timeout 3 nb_get_retry 3 delay_before_retry 3 connect_port 80 } } }

persistence_timeout 50表示同一客户端50秒内的请求都打到同一后端,这是一种简单的会话保持。TCP_CHECK会定期探测后端80端口,连续失败3次就把该后端从池子里摘掉。

查看LVS转发状态用ipvsadm -Ln,输出里会显示每个后端的活跃连接数和权重。如果发现某个后端一直是0连接,要么是健康检查没通过,要么是权重配错了。

实操心得:DR模式下,后端服务器的默认网关不需要指向调度器,因为响应是直接回给客户端的。但如果你发现响应回不去,检查后端服务器的路由表,确保回包路径正确。

4. HAproxy:七层代理的健康检查与高可用实践

4.1 HAproxy和LVS的本质区别:四层与七层的分界线

LVS工作在四层(传输层),它只看IP和端口,不关心内容是HTTP还是MySQL。HAproxy工作在七层(应用层),它能解析HTTP协议,根据URL、Host头、Cookie做转发决策,还能做SSL终止、压缩、重写。

打个比方:LVS像一个邮局分拣员,只看信封上的地址把信分到不同区域;HAproxy像一个前台接待,能拆开信封看内容,根据信里写的事情类型转给不同的部门。所以HAproxy更灵活但性能略低,LVS更高性能但功能单一。实际架构中经常是:LVS做第一层四层负载均衡,后面挂多个HAproxy做七层转发,HAproxy后面才是真正的应用服务器。

4.2 健康检查的三种级别:从端口探测到业务级检测

HAproxy的健康检查比LVS精细得多,分三个层次:

  • TCP检查:只检查端口是否能建立连接。配置option tcp-check。
  • HTTP检查:发送HTTP请求,检查返回状态码。配置option httpchk GET /health。
  • 业务级检查:检查返回内容里是否包含特定字符串。配置http-check expect string "OK"。

我强烈建议至少用HTTP检查,因为端口开着不代表服务正常。曾经遇到过Tomcat进程还在但线程池满了,端口能连上但请求全部超时,TCP检查完全发现不了。加上http-check expect status 200后,这种假死状态就能被及时摘除。

4.3 HAproxy配置文件的结构与关键参数

HAproxy配置分四段:global、defaults、frontend、backend。下面是一个生产可用的模板:

global log /dev/log local0 maxconn 50000 user haproxy group haproxy daemon stats socket /var/run/haproxy.sock mode 600 level admin defaults log global mode http option httplog option dontlognull option redispatch retries 3 timeout connect 5s timeout client 30s timeout server 30s timeout check 3s frontend web_front bind *:80 bind *:443 ssl crt /etc/haproxy/certs/site.pem default_backend web_servers backend web_servers balance roundrobin option httpchk GET /health http-check expect status 200 server web1 192.168.1.20:8080 check inter 2s fall 3 rise 2 server web2 192.168.1.21:8080 check inter 2s fall 3 rise 2 backup

几个关键参数解释:option redispatch表示如果某台后端挂了,重试时把请求发给其他健康后端;retries 3是重试次数;inter 2s fall 3 rise 2表示每2秒检查一次,连续3次失败标记为down,连续2次成功标记为up;backup标记该服务器为备用,只有主服务器全挂才启用。

4.4 用Keepalived给HAproxy做高可用

HAproxy本身不做高可用,需要Keepalived配合。配置和第二节的Keepalived热备几乎一样,只是健康检查脚本改成检查HAproxy进程:

vrrp_script chk_haproxy { script "killall -0 haproxy" interval 2 weight -30 fall 3 rise 2 }

两台HAproxy都配置相同的backend,VIP在Master上。客户端访问VIP,Master上的HAproxy处理请求。Master挂了,VIP漂到Backup,Backup上的HAproxy接管。由于两台HAproxy配置完全一致,切换后行为不变。

注意:HAproxy的stats socket可以用来做运行时管理,比如动态调整后端权重、下线某台服务器。配合socat工具可以脚本化操作,这在紧急故障时非常有用。

5. 三种方案的选型对比与组合架构

5.1 一张表看清三种方案的适用边界

维度Keepalived热备Keepalived+LVSHAproxy
工作层次节点级四层七层
核心功能VIP漂移、节点冗余四层负载均衡+高可用七层转发+健康检查
性能不涉及转发,无损耗内核级,极高用户态,较高
灵活性低低高
配置复杂度低中中
典型场景数据库主备、单服务双机大流量四层分发HTTP路由、SSL终止、灰度发布
会话保持不涉及源地址哈希Cookie插入/源地址哈希

选型的核心判断逻辑是:先问需不需要七层功能,再问流量规模有多大。如果只需要把请求分给几台后端且不需要看HTTP内容,LVS+Keepalived是性能和稳定性的最优解。如果需要根据URL路由、做SSL终止、或者后端是HTTP服务且需要精细健康检查,HAproxy更合适。如果只是两台机器做主备(比如MySQL主从),Keepalived热备就够了。

5.2 大规模场景下的组合架构:LVS+HAproxy+Keepalived

在日活百万级以上的场景里,常见架构是三层:

  1. 最外层:两台LVS调度器 + Keepalived,持有VIP,用DR模式把流量分给下层HAproxy集群。
  2. 中间层:多台HAproxy,每台都用Keepalived做双机热备(或者用ECMP等价路由做集群),负责七层转发和健康检查。
  3. 最内层:应用服务器集群,注册在HAproxy的backend里。

这样LVS扛住了四层的高并发,HAproxy提供了七层的灵活性,Keepalived保证了每一层都没有单点。每一层的健康检查独立工作:LVS检查HAproxy的端口,HAproxy检查应用的健康接口,任何一层出问题都能自动隔离。

这个架构的复杂度在于故障排查链路变长。请求不通时,要逐层确认:VIP在不在、LVS转发规则对不对、HAproxy后端状态是否健康、应用本身是否正常。我的习惯是每层都配好日志和监控,LVS用ipvsadm -Ln --stats看转发计数,HAproxy用stats页面看后端状态,应用层看自己的健康接口。

5.3 云环境下的特殊考量

在云平台上,VIP漂移需要平台支持。有些云厂商允许在控制台申请虚拟IP并绑定到实例,有些则限制ARP响应,导致Keepalived默认的组播模式无法工作。这时候要么改用单播模式,要么使用云厂商提供的负载均衡服务。

另外,云环境的网络延迟和丢包率与物理机房不同,VRRP的advert_int可能需要调大,避免网络抖动导致误切换。我一般会在云环境把advert_int设为2秒,fall设为3次,这样需要6秒才判定故障,虽然切换慢一点,但避免了频繁误切。

6. 实战排查:那些配置正确但就是不工作的时刻

6.1 VIP漂移了但服务不通:ARP缓存惹的祸

有一次切换后,VIP确实漂到了备节点,ip addr能看到VIP,但客户端访问就是不通。排查了半天发现是客户端和交换机的ARP缓存还指向旧Master的MAC。VRRP切换时会发送免费ARP(gratuitous ARP)通知大家更新缓存,但如果网络设备忽略了免费ARP,缓存就不会更新。

解决办法有两个:一是在Keepalived配置里加garp_master_delay和garp_master_repeat参数,多发送几次免费ARP;二是在切换脚本里手动发送ARP更新:

arping -I eth0 -c 3 -A 192.168.1.100

-A表示发送ARP应答而不是请求,-c 3表示发3次。这个命令放在notify_master脚本里,能大幅提高切换后立即恢复的概率。

6.2 HAproxy后端全部显示DOWN但服务明明正常

这种情况通常是健康检查配置和后端实际行为不匹配。比如后端返回的是302重定向,但健康检查期望200;或者后端要求Host头,但健康检查没带。排查步骤:

  1. 在HAproxy服务器上手动执行健康检查请求:curl -v http://后端IP:端口/health,看返回什么。
  2. 检查HAproxy日志:tail -f /var/log/haproxy.log,看健康检查的具体失败原因。
  3. 用stats socket查看后端详情:echo "show stat" | socat stdio /var/run/haproxy.sock。

常见修复方式:加http-check expect status 200 302允许多个状态码,或者在option httpchk里指定完整的请求头和Host。

6.3 LVS转发正常但后端收不到请求

DR模式下,如果后端收不到请求,先检查后端lo接口上的VIP是否配置正确,以及ARP参数是否生效。用ip addr show lo确认VIP在,用sysctl net.ipv4.conf.all.arp_ignore确认值是1。还有一个容易忽略的点:后端服务器的防火墙可能拦截了目标IP为VIP的包。因为包的目标IP是VIP,而VIP配置在lo上,某些防火墙规则会把它当作非法流量丢弃。检查iptables -L -n或firewall-cmd --list-all,确保没有拦截。

6.4 Keepalived日志里反复出现“VRRP_IS_INITIALIZED”错误

这个错误通常表示VRRP实例初始化失败,最常见的原因是virtual_router_id冲突或者网卡名称写错。用ip link show确认网卡名,用tcpdump -i eth0 vrrp抓包看是否有其他VRRP通告。如果同一网段有多个Keepalived组,确保每组的router_id不同。

7. 监控与日常维护:让高可用真正“可用”

7.1 必须监控的五个指标

高可用架构搭好只是开始,没有监控的高可用等于没有高可用。以下五个指标必须纳入监控:

  • VIP归属:当前VIP在哪个节点上,是否与预期一致。可以用脚本定期检查ip addr | grep VIP并上报。
  • VRRP状态:Keepalived实例是MASTER还是BACKUP,状态切换次数。
  • 后端健康状态:HAproxy或LVS里有多少后端处于down状态。
  • 切换事件:每次VIP漂移都要记录并告警,因为非预期的切换意味着有故障发生。
  • 连接数:LVS的活跃连接数、HAproxy的当前会话数,突增或突降都可能是异常信号。

7.2 定期演练:不要等真出事才发现切换不了

我见过太多团队配了Keepalived但从来没测试过切换,真到主节点挂了才发现备节点配置有误、VIP漂不过去。建议每季度至少做一次切换演练:手动停掉主节点的Keepalived,观察VIP是否在预期时间内漂到备节点,业务是否自动恢复,切换期间有多少请求失败。

演练时要注意:选择业务低峰期,提前通知相关方,准备好回滚方案。切换后检查所有后端服务的会话是否丢失、数据库连接是否正常。演练结束后分析切换耗时,如果超过预期,检查advert_int、fall等参数是否合理。

7.3 配置版本管理:别让两台机器的配置不一致

Keepalived和HAproxy的配置文件必须纳入版本管理(如Git),每次修改都提交并记录原因。两台机器的配置差异应该只有state、priority、router_id这几项,其他必须完全一致。我习惯用Ansible或类似的配置管理工具批量下发,避免手动修改导致不一致。

实操心得:在notify_master、notify_backup脚本里加上配置校验逻辑,比如检查关键进程是否在运行、关键端口是否监听。如果校验失败,脚本返回非零值,Keepalived会认为切换失败并记录日志,避免切到一个不健康的节点上。

8. 我在这套架构上踩过的三个真实坑

第一个坑是优先级设置反了。主节点priority配了90,备节点配了100,结果备节点一直抢着当Master,主节点反而成了Backup。当时没仔细看日志,排查了半天才发现是数字大小搞反了。记住:priority越大越优先。

第二个坑是健康检查脚本权限问题。Keepalived默认以keepalived用户运行脚本,但脚本里执行的命令需要root权限,导致检查一直失败。后来在global_defs里加了script_user root和enable_script_security才解决。但要注意,用root跑脚本有安全风险,脚本内容必须严格可控。

第三个坑是HAproxy的timeout设置过短。后端某个接口处理时间较长(比如导出报表要10秒),但timeout server设了5秒,导致请求被HAproxy主动断开,用户看到502。后来把超时调到30秒,并对长耗时接口单独配置了更长的超时。这个问题的教训是:超时时间要根据业务实际响应时间来定,不能拍脑袋。

这套高可用架构从单机到双机、从四层到七层,每一步都是在解决具体问题的过程中逐步演进的。没有银弹,只有适合当前场景的组合。Keepalived负责“谁活着”,LVS负责“分得快”,HAproxy负责“分得准”,三者各司其职,配合好了才能让服务真正稳定运行。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 5:49:23

多路摄像头采集架构:多线程读取与多进程保存实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:49:21

Redis Stream遍历原理与工程实践:时间轴游标、消费者组与精准回溯

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:48:31

傅里叶变换工程实战:从DFT到FFT的频谱分析避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:48:19

Jordan公式真相:概率论中的容斥原理与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:47:58

Python JSON文件读写实战:编码异常与性能优化指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:47:57

2025年Fortran实战指南:从环境搭建到高性能数值计算

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华