Floodlight OpenFlow控制器源码与模块开发实战

发布时间:2026/10/23 14:26:51
Floodlight OpenFlow控制器源码与模块开发实战 简介面向SDN学习者与Java开发者的Floodlight控制器安装部署资料聚焦开源SDN控制器的环境准备与实际部署适合希望在Ubuntu/Linux或虚拟机中搭建实验环境、理解控制器运行方式的初中级读者。资源包体积约64.72MB文件总数与类型明细上游暂未提供计数显示为0实际内容以压缩包为准从描述看资料围绕Floodlight的安装部署展开可帮助读者建立对控制器角色、运行环境与基础配置的整体认识。已有554人学习说明其在SDN入门与实验搭建场景中具有一定参考价值。读者可借助该资料减少环境准备阶段的盲目尝试梳理部署思路与排错方向并为后续SDN网络实验和应用扩展打下基础。1. Floodlight 是什么一份用 Java 写的 OpenFlow 控制器源码值不值得拆Mininet 起来之后交换机一直停在 “waiting for switches” 那一行很多人的第一反应是拓扑写错了实际上是控制器没接住。Floodlight 就是解决这一层问题的东西一份 Apache 2.0 的 Java OpenFlow 控制器实现自带模块容器、REST 管理接口和 OpenFlow 1.0/1.3 协议栈跑起来之后你能从控制器侧决定一个包该往哪个口走。它适合三类人要做 SDN 转发逻辑二次开发的、要在控制器上挂 REST 做编排实验的、要拿一份能编译能改的 Java 工程当参照物的。不适合只想敲两条命令看拓扑图的那种场景用现成的轻量脚本更省事。它真正的价值不是“能跑”是模块之间那套依赖注入和消息订阅机制——你写一个类注册进去就能在 Packet-In 链路上插一脚。2. 从源码到跑起来JDK、构建链与三处必改配置拿到一份控制器源码最怕的不是看不懂代码是编译过了、启动没报错、但交换机就是连不上。这一章把“能编译 → 能启动 → 能收连接”这条链路拆成三段每段给一个可验证的判据。2.1 版本对齐JDK、构建工具与协议栈依赖Floodlight 是 Java 工程但它对 JDK 版本相当挑剔。老分支围绕 JDK 8 写依赖里的 Guice、Netty 和 Jython 都用了反射去访问 JDK 内部结构到了 JDK 17 之后模块系统收紧启动阶段大概率甩一个InaccessibleObjectException堆栈指向反射调用点跟你的业务代码一点关系没有。我一般把基线定在 JDK 11既能用上新语法又不至于天天跟--add-opens较劲。构建工具有两套并存的可能仓库里如果留着build.xml说明这条分支走 Ant如果有pom.xml走 Maven。两者别混用混用的典型症状是target/下的产物一半是新的一半是旧的跑起来行为诡异。组件建议取值为什么出问题的表现JDK118 也可反射与模块系统的兼容区间启动即崩堆栈含反射类构建工具与仓库描述文件一致两套产物目录会互相污染改了代码行为不变OpenFlow 版本1.3 为主1.0 兼容大部分虚拟交换机默认 1.3交换机握手后立刻断开内存堆不小于 1G流表缓存和拓扑对象吃内存长时间跑后 OOM提示先执行java -version和mvn -v把两个版本号记下来。后面所有“莫名其妙”的启动失败第一个要排除的就是这两个数。依赖下载慢是常态本地仓库里如果已经有一份完整的~/.m2直接复用比重下快一个数量级。不要为了图快去改依赖版本号控制器对 Netty 和 Jackson 的版本相当敏感改一个会连带一串序列化问题。2.2 构建与首次启动命令、启动日志与失败判读确认版本之后构建就是一条命令的事。下面这套流程我跑过很多次重点是每一步都有可观测的输出不要一口气跑完再看结果。# 1. 清理并跳过测试打包测试用例里有些依赖外部环境先不跑 mvn -q clean package -DskipTests # 2. 确认产物存在且是可执行 jar ls -lh target/*.jar unzip -p target/floodlight.jar META-INF/MANIFEST.MF | grep -i main-class # 3. 前台启动日志直接打到终端方便第一时间看异常 java -Xms512m -Xmx1g -jar target/floodlight.jar第一行-DskipTests是有意为之测试阶段可能有集成用例去连真实端口机器上没有环境就会失败把打包卡住。第二步的MANIFEST.MF检查很关键能确认打出来的 jar 带了Main-Class否则java -jar会报 “no main manifest attribute”这时候要么是打包插件配置被改过要么是打成了普通依赖 jar。第三步的-Xms和-Xmx显式给值避免不同机器默认堆大小差异导致“我这能跑你那不能跑”的玄学。启动成功的日志里会出现几类关键行模块逐个加载的记录、REST 服务监听的端口、OpenFlow 服务监听的端口。缺哪一类问题就在哪一段。构建失败的常见原因集中在两处一是依赖仓库里有 SNAPSHOT 版本拉不到报Could not resolve dependencies二是编译插件指定的源码级别和你本地 JDK 不匹配报invalid target release。前者去pom.xml里看依赖坐标后者改编译插件的source/target或者换 JDK。2.3 三处必改配置OpenFlow 端口、REST 端口、模块清单配置文件的默认值往往和你的实验环境对不上改动量不大但必须动。在资源目录下找floodlightdefault.properties这类文件里面是 keyvalue 的纯文本改完重新打包或者用外部配置覆盖都行。# OpenFlow 监听端口Mininet 默认往 6633 发控制器新版默认听 6653 # 两边必须一致否则现象是交换机反复重连、日志里看不到 hello 握手 net.floodlightcontroller.core.FloodlightProviderService.openFlowPort6653 # REST 管理端口8080 极易被本地其他服务占用 net.floodlightcontroller.restserver.RestServer.restPort8080 # 模块清单写在这里的模块才会被加载换行用反斜杠续行 # 自己写的模块类全限定名必须追加到末尾否则代码在包里也不会被实例化 floodlight.modules\ net.floodlightcontroller.core.FloodlightProvider,\ net.floodlightcontroller.restserver.RestServer,\ net.floodlightcontroller.topology.TopologyManager,\ net.floodlightcontroller.samplefwd.SampleForwarding端口这两项的键名在不同分支上可能有细微差异改之前先在配置目录里grep -rn Port扫一遍以实际存在的键为准别照抄一个不存在的键然后困惑为什么没生效。配置文件里写了但不被识别的键会被静默忽略这是最容易翻车的地方——你以为改了其实根本没读。模块清单是三项里最要紧的。控制器的模块容器只实例化清单里列出的类你新加的模块不进清单代码写得再对也不会被调用表现是 REST 接口 404、日志里没有对应的加载记录。清单用逗号分隔末尾不要留多余逗号续行的反斜杠后面不能有空格。改完启动验证方式很直接日志里搜你模块的类名能搜到加载记录说明进清单了再curl一下 REST 端口能返回 JSON 说明服务起来了。两个都通过才谈得上下一步。3. 模块机制拆解写一个能收 Packet-In 并下发流表的 Java 模块能跑起来只是及格线。真正的分水岭在于你能不能把自己的转发逻辑塞进控制器的主链路。Floodlight 的扩展方式是“实现接口 注册服务”没有插件目录扫描那一套全部在编译期确定。3.1 模块生命周期与依赖注入startUp 里到底该做什么一个模块要实现IFloodlightModule这个接口里有五个方法值得逐个说清楚。getModuleServices()返回本模块对外暴露的服务接口不对外暴露就返回 nullgetServiceImpls()返回接口到实现的映射getModuleDependencies()声明依赖容器会据此决定加载顺序init()做资源准备startUp()做真正的注册动作。新手最容易犯的错是在init()里就去订阅消息。这时候依赖的提供者模块可能还没完成自己的startUp()你拿到的服务对象是半初始化状态注册上去的回调会在某个不确定的时刻炸掉。规矩是init()里只取服务引用、读配置、初始化本地数据结构startUp()里才注册监听器、挂 REST 路由。依赖注入用的是 Guice你在getModuleDependencies()里写的接口列表容器保证在调用你的init()之前已经把对应实现准备好context.getServiceImpl(...)拿到的就是可用对象。这套机制的好处是模块之间不需要知道彼此的具体类名坏处是依赖关系写错了不会在编译期报错只会表现为运行时拿到 null。还有一个隐性约束消息监听是有顺序的。isCallbackOrderingPrereq()和isCallbackOrderingPostreq()这两个方法决定你的监听器排在别的监听器之前还是之后。比如你要做转发决策就必须排在那些只做统计记录的监听器前面否则别人先处理完返回了 STOP你的回调压根不会被调用。3.2 从 Packet-In 到 FlowMod一份可编译的最小转发模块下面这个类实现了一个最朴素的 MAC 学习转发收到 Packet-In 记录源 MAC 与入端口目的 MAC 已知就下发流表让后续同流量不再上送未知就泛洪。代码可以直接放进工程里编译。package net.floodlightcontroller.samplefwd; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.projectfloodlight.openflow.protocol.OFFlowAdd; import org.projectfloodlight.openflow.protocol.OFMessage; import org.projectfloodlight.openflow.protocol.OFPacketIn; import org.projectfloodlight.openflow.protocol.OFType; import org.projectfloodlight.openflow.protocol.action.OFAction; import org.projectfloodlight.openflow.protocol.match.Match; import org.projectfloodlight.openflow.protocol.match.MatchField; import org.projectfloodlight.openflow.types.OFBufferId; import org.projectfloodlight.openflow.types.OFPort; import net.floodlightcontroller.core.FloodlightContext; import net.floodlightcontroller.core.IFloodlightProviderService; import net.floodlightcontroller.core.IOFMessageListener; import net.floodlightcontroller.core.IOFSwitch; import net.floodlightcontroller.core.module.FloodlightModuleContext; import net.floodlightcontroller.core.module.FloodlightModuleException; import net.floodlightcontroller.core.module.IFloodlightModule; import net.floodlightcontroller.core.module.IFloodlightService; import net.floodlightcontroller.packet.Ethernet; public class SampleForwarding implements IOFMessageListener, IFloodlightModule { private IFloodlightProviderService floodlightProvider; // 用 DPID 做 key别直接用 IOFSwitch 对象做 key交换机重连后对象会换 private final MapLong, MapLong, OFPort macTable new HashMap(); Override public CollectionClass? extends IFloodlightService getModuleServices() { return null; // 本模块不对外提供服务 } Override public MapClass? extends IFloodlightService, IFloodlightService getServiceImpls() { return null; } Override public CollectionClass? extends IFloodlightService getModuleDependencies() { return Collections.singletonList(IFloodlightProviderService.class); } Override public void init(FloodlightModuleContext context) throws FloodlightModuleException { // 只取引用不做注册 floodlightProvider context.getServiceImpl(IFloodlightProviderService.class); } Override public void startUp(FloodlightModuleContext context) throws FloodlightModuleException { // 只订阅 PACKET_IN其余类型交给别的模块处理 floodlightProvider.addOFMessageListener(OFType.PACKET_IN, this); } Override public String getName() { return samplefwd; } Override public Command receive(IOFSwitch sw, OFMessage msg, FloodlightContext cntx) { if (msg.getType() ! OFType.PACKET_IN) { return Command.CONTINUE; // 不是自己关心的类型放行给下一个监听器 } OFPacketIn pi (OFPacketIn) msg; Ethernet eth IFloodlightProviderService.bcStore .get(cntx, IFloodlightProviderService.CONTEXT_PI_PAYLOAD); OFPort inPort pi.getMatch().get(MatchField.IN_PORT); long src Ethernet.toLong(eth.getSourceMACAddress().getBytes()); long dst Ethernet.toLong(eth.getDestinationMACAddress().getBytes()); macTable.computeIfAbsent(sw.getId().getLong(), k - new HashMap()).put(src, inPort); OFPort outPort macTable.get(sw.getId().getLong()).get(dst); if (outPort null || outPort.equals(inPort)) { // 未知单播 / 源目同口直接泛洪不下流表 OFAction flood sw.getOFFactory().actions().buildOutput() .setPort(OFPort.FLOOD).setMaxLen(0).build(); sw.write(sw.getOFFactory().buildPacketOut() .setBufferId(OFBufferId.NO_BUFFER) .setInPort(inPort) .setActions(Collections.singletonList(flood)) .setData(pi.getData()) .build()); return Command.CONTINUE; } // 目的口已知装一条匹配 ETH_DST 的流表后续同流量不再上送 Match match sw.getOFFactory().buildMatch() .setExact(MatchField.ETH_DST, eth.getDestinationMACAddress()) .build(); OFAction output sw.getOFFactory().actions().buildOutput() .setPort(outPort).setMaxLen(0).build(); OFFlowAdd flowAdd sw.getOFFactory().buildFlowAdd() .setMatch(match) .setActions(Collections.singletonList(output)) .setPriority(100) .setIdleTimeout((short) 30) // 30 秒无流量自动老化 .setHardTimeout((short) 0) // 0 表示不设硬超时 .setBufferId(OFBufferId.NO_BUFFER) .build(); sw.write(flowAdd); return Command.CONTINUE; } Override public boolean isCallbackOrderingPrereq(OFType type, String name) { return false; // 不强制排在谁前面 } Override public boolean isCallbackOrderingPostreq(OFType type, String name) { return false; } }receive()的返回值是Command枚举CONTINUE表示继续交给下一个监听器STOP表示到此为止。做转发决策的模块一般返回CONTINUE只有当你确定这条消息不该被别人看到时才返回STOP否则会把统计、拓扑发现这类模块的输入掐断症状是拓扑图莫名其妙少链路。setPriority(100)这个值有讲究控制器自己会装一些默认流表比如 table-miss 表项优先级通常是 0你的表项必须高于它才能命中。如果发现流表装进去了但计数器一直是 0先查优先级是不是被别的表项压住了。OFBufferId.NO_BUFFER表示让交换机不要缓存这个包Packet-In 的消息体里带着完整数据。带 buffer 的做法能省带宽但交换机缓存有生命周期你稍慢一点回 FlowModbuffer 就失效了包直接丢——这个坑后面还会提。setIdleTimeout((short) 30)是老化时间单位秒。设太短会频繁重装表项设太长会让 MAC 表迁移后转发路径迟迟不更新。三十秒是个折中起点实际按业务流量特征调。3.3 挂 REST 接口把内部状态暴露成 JSON光有转发逻辑不够你还得能看见模块内部在想什么。Floodlight 的 REST 层基于 Restlet扩展方式是继承RestletRoutable在getRestlet()里把 URL 路径绑到资源类上。package net.floodlightcontroller.samplefwd; import org.restlet.Context; import org.restlet.Restlet; import org.restlet.routing.Router; import net.floodlightcontroller.restserver.RestletRoutable; public class SampleWebRoutable extends RestletRoutable { Override public Restlet getRestlet(Context context) { Router router new Router(context); // 路径直接拼在 basePath 后面 router.attach(/mactable/json, MacTableResource.class); return router; } Override public String basePath() { return /sample; // 最终访问路径 /wm/sample/mactable/json } }路由注册只是第一步还得让控制器知道这个 Routable 的存在。常见做法是在模块的startUp()里取IRestApiService的引用然后调用addRestletRoutable()把实例挂上去。这一步漏掉的话路径写对了也是 404。Override public void startUp(FloodlightModuleContext context) throws FloodlightModuleException { floodlightProvider context.getServiceImpl(IFloodlightProviderService.class); floodlightProvider.addOFMessageListener(OFType.PACKET_IN, this); // 挂 REST 路由缺这一步就是 404 IRestApiService restApi context.getServiceImpl(IRestApiService.class); restApi.addRestletRoutable(new SampleWebRoutable()); }注意basePath()返回的字符串最终会被拼上/wm前缀所以实际访问地址是/wm/sample/mactable/json。资源类里的序列化一般用 Jackson返回对象直接写new JacksonRepresentation(obj)就行不需要手拼 JSON 字符串。验证方式是启动后用curl打一下看返回体里有没有你预期的字段。如果返回 404按这个顺序查模块有没有进floodlight.modules、startUp()有没有调用addRestletRoutable、basePath()和attach()拼出来的路径跟你curl的是不是同一个。这三处任何一处不对都表现为 404光看堆栈是看不出来的。4. Mininet 闭环验证拓扑、流表与 REST 三面对账代码写完不等于逻辑对。控制器的特殊性在于它的输出是流表而流表生效与否只能靠数据面行为倒推。这一章给一套核对方法拓扑面确认连接REST 面确认状态抓包面确认消息次数。4.1 起一个最小拓扑并确认交换机上线验证用的虚拟网络我用 Mininet四个主机、两台交换机足够看清路径。命令行里显式指定控制器地址和端口别依赖默认值——Mininet 的默认端口和控制器默认端口经常不是一个数。# tree,2 生成两级树4 台主机 3 台交换机够看转发路径 # protocols 显式指定 OpenFlow 1.3避免协商降级 sudo mn --controllerremote,ip127.0.0.1,port6653 \ --topotree,2 \ --switchovsk,protocolsOpenFlow13命令起完之后别急着敲pingall。先在 Mininet 里执行switches看交换机列表然后再看控制器侧的日志有没有出现对应数量的交换机连接记录。两边数量对不上说明有交换机没连上这时候 ping 一定是不通的先解决连接问题再谈转发。如果 Mininet 卡在 “waiting for switches”八成是端口不匹配。控制器听 6653、Mininet 往 6633 发或者反过来都是同一个表现。另一种可能是控制器进程根本没活netstat -lnp | grep 6653看一眼端口在不在监听最直接。4.2 用 REST 查拓扑、链路和流表控制器起来之后REST 就是你的黑匣子读出口。这几个接口是排查时的固定动作建议存成一个小脚本出问题先跑一遍。# 1. 交换机是否都在返回 JSON 数组长度应等于拓扑里的交换机数 curl -s http://127.0.0.1:8080/wm/core/switches/json | python3 -m json.tool # 2. 链路发现是否正常返回带 src/dst 的链路列表 curl -s http://127.0.0.1:8080/wm/topology/links/json | python3 -m json.tool # 3. 某台交换机的流表把 1 换成目标交换机 DPID 的末位 curl -s http://127.0.0.1:8080/wm/core/switch/1/flow/json | python3 -m json.tool第一条返回空数组[]说明控制器一个交换机都没连上问题在连接层不在转发层。第二条返回空但第一条有内容通常是链路发现模块没加载或者虚拟交换机禁用了 LLDP——拓扑发现靠的就是周期性发 LLDP 包。第三条查流表最有用。你会看到两类表项控制器自己装的默认表项和你模块装的业务表项。业务表项的match字段里应该能看到你设置的eth_dstpriority是 100actions里是一个output动作且端口号正确。如果连表项都没有说明 FlowMod 压根没发出去回去查receive()有没有被调用。注意/wm/core/switch/1/flow/json里的那个1是交换机 DPID 的十进制末位写法不同分支对路径参数的解析方式不完全一样。拿不准的时候先用第一条接口把 DPID 原样取出来再拼进路径。4.3 抓包与日志对账确认 Packet-In 只来一次转发逻辑对不对最有说服力的证据是“同一个流的 Packet-In 只来一次”。第一次上来装表后续包在数据面直接命中不该再上送。这个行为用抓包和日志两条线对账最清楚。# 在控制器侧抓 OpenFlow 端口过滤出 packet_in 消息 sudo tcpdump -i lo -n port 6653 -w /tmp/of.pcap # 另一侧发起一对主机的通信 mininet h1 ping -c 5 h2 # 抓完之后统计 packet_in 出现次数 tcpdump -r /tmp/of.pcap -nn 2/dev/null | grep -c packet_in预期结果是h1 ping h2五个包Packet-In 次数应该远少于五次理想情况下是两次一次 ARP、一次 ICMP 首包后续包命中流表。如果五次 ping 对应五次以上 Packet-In说明流表没命中回去看match字段和优先级。日志侧在模块里打点更直接在receive()入口加一行计数日志跑完统计条数。日志级别调到 INFO 就够别开 DEBUGPacket-In 密集的时候 DEBUG 日志本身就是性能瓶颈会把你要观察的行为改变掉。对账时还要注意一个容易误判的点ARP 包和 ICMP 包是两条不同的流。你看到两次 Packet-In 是正常的分别对应 ARP 解析和 ICMP 首包。很多人一看“两次”就以为流表没生效其实是协议层本来就有两条流。5. 避坑排查五条让控制器“假活”的细节“假活”是控制器调试里最耗时间的形态进程在、端口在监听、日志没报错但功能就是不对。下面五条是我踩过并且反复见到别人踩的每条给出判断依据和处理路径。第一条Mininet 一直停在 waiting for switches控制器日志干干净净。现象是 Mininet 侧反复重试控制器侧一条连接记录都没有。原因是两边的 OpenFlow 端口号不一致旧习惯用 6633新版本默认 6653一边改了一边没改。解决办法是先把控制器配置里的端口读出来再在 Mininet 命令里显式写port同一个值不要依赖任何一方的默认值。验证方法是在控制器机器上netstat -lnp | grep 端口确认监听存在之后再去 Mininet 里重试。第二条启动几秒后进程退出日志末尾是绑定异常。现象是java -jar之后终端回到提示符翻日志最后几十行能看到端口已被占用的错误。原因是 REST 端口 8080 被本地别的服务占了容器启动阶段绑定失败直接终止。解决方式是换端口改配置里的 restPort 为一个空闲值或者先把占用者停掉。这里有个容易忽略的点端口被占导致的启动失败不会给你一个“友好”的提示它会甩一段堆栈然后退出不看日志尾部根本发现不了。启动之后养成ps确认进程还在的习惯。第三条REST 接口 404日志里搜不到自己模块的名字。现象是curl返回 404控制器其他接口正常。原因是模块类没写进floodlight.modules清单容器压根没实例化它。解决办法是把类的全限定名追加到清单末尾注意反斜杠续行后面不能有空格、末尾不能有多余逗号。验证方式是重启后 grep 日志里的类名有加载记录才算进清单。第四条启动瞬间抛反射相关异常堆栈指向依赖库内部。现象是InaccessibleObjectException或者IllegalAccessError堆栈里的类名你一个都不认识。原因是高版本 JDK 的模块系统限制了对内部 API 的反射访问而依赖库还在用老路子。解决办法优先降 JDK 到 11实在要用高版本就补充--add-opens参数把用到的包打开。这条坑的迷惑性在于它跟你的业务代码零关系但会让你怀疑自己改错了东西。第五条流表装进去了但计数器一直是 0流量不通。现象是 REST 查流表能看到你装的表项字段都对唯独packet_count不增长。三个常见原因优先级低于已有的默认表项被压住match里用了交换机不支持的匹配字段动作里的输出端口号写错了比如本想输出到物理口 1实际写成了逻辑端口。排查顺序是先看流表里表项的排列顺序和优先级再用交换机的本地工具 dump 一遍流表做对照最后核对端口号的取值方式。6. 进阶把控制器从“能跑”推到“能扛”的两把尺子功能对了之后下一个问题是它能不能扛住。控制器这类组件的性能瓶颈通常不在业务逻辑而在消息处理线程和流表下发频率上。我给两个可量化的尺子都是能自己跑出来的数。第一把尺子是 Packet-In 处理延迟。在receive()的首尾各取一次System.nanoTime()把差值累加到一个LongAdder里通过 REST 暴露出去。跑一轮固定速率的流量观察 P50 和 P99 的差距。P99 突然飙高而 P50 平稳基本可以判定是某类消息触发了锁竞争或者大对象分配这时候用jstack抓几份线程栈对比能定位到具体卡在哪个方法上。我一般连续抓三份、间隔两秒看有没有线程长期停在同一个栈帧。第二把尺子是流表下发速率。FlowMod 是异步写的sw.write()返回不代表交换机处理完堆积起来会撑爆发送队列。做法是给下发动作加一个轻量计数窗口统计每秒下发条数同时通过 REST 读交换机的流表总数。两者曲线如果出现“下发数飙升但流表总数不涨”的背离说明交换机在拒绝或者合并表项继续加压只会让队列越堆越长。观察项采集方式异常信号通常对应的原因Packet-In 处理耗时模块内计时 REST 输出P99 明显高于 P50锁竞争或大对象分配流表下发速率模块内计数窗口下发数涨、流表数不涨交换机侧拒绝或表项合并交换机连接数已有 REST 接口周期性掉零再恢复心跳超时或端口抖动堆内存曲线JVM 自带监控锯齿变陡、老年代不回落表项缓存无上限增长一个具体技巧MAC 表这类结构一定要设上限。我见过一个模块用普通HashMap无界缓存源 MAC实验室里跑了几小时没事接了一段扫描流量之后内存直接顶到上限进程被容器杀掉现象看起来像“控制器自己挂了”实际是缓存撑爆。改成带容量上限的结构超限时按最近最少使用淘汰问题就消失了。从那以后我每次加一个新的缓存结构都强制走一遍“上限是多少、淘汰策略是什么、满了之后降级行为是什么”这三问答不上来就先不加。这套习惯比任何性能调优技巧都省事。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询