
1. 项目缘起为什么需要指定服务器进行测速作为一名经常和服务器打交道的运维我几乎每天都要和网络质量打交道。无论是评估新机房的线路还是排查用户反馈的卡顿问题一个准确、快速的网络测速工具是必不可少的。speedtest-cli这个由 Ookla 官方提供的命令行工具就成了我工具箱里的常客。它基于和网页版 Speedtest 相同的后端数据相对权威用起来也方便一条命令speedtest-cli就能跑起来。但用久了一个痛点就浮现出来了它默认自动选择“最优”服务器但这个“最优”往往不是我想测的那个。比如我想测试从上海到北京某个特定 IDC 机房的延迟和带宽但工具可能自动连到了广州甚至海外的节点测出来的数据对我当下的需求毫无参考价值。官方虽然提供了--list参数来展示服务器列表再用--server指定 ID 的方式但这个过程还是太繁琐了。每次都得先列出来在一长串混杂着国内外、各种运营商的列表里用眼睛找再记下 ID最后再执行测试。脚本自动化想都别想因为服务器列表和 ID 可能会变。所以这个项目的目标很明确改造speedtest-cli让它能通过我预定义的、易于维护的方式比如配置文件或命令行参数直接指定我要测试的目标服务器实现一键精准测速。这不仅仅是偷懒更是为了将网络质量监控集成到自动化流程中的关键一步。2. 原版 speedtest-cli 的工作机制与局限要修改它首先得吃透它。原版speedtest-cli是一个 Python 脚本它的工作流程可以拆解为以下几个核心步骤理解了这些我们才知道该在哪里动刀。2.1 核心流程拆解获取配置文件 (speedtest-cli --version时触发)脚本启动后首先会尝试从预定义的 URL 下载一个speedtest-configuration配置文件。这个文件包含了测速所需的所有基础信息尤其是servers列表的详细信息。这个列表是一个庞大的 JSON 数组包含了全球成千上万个 Speedtest 服务器节点每个节点都有id、name、host、sponsor、country、cc、lat、lon等字段。这个列表是动态更新的保证了工具的时效性。计算地理距离与排序脚本会获取或估算运行主机的公网 IP 和地理坐标。然后它遍历上一步获取到的全球服务器列表计算当前主机与每一个服务器之间的地理距离通常使用 Haversine 公式。接着它会根据距离由近到远进行排序。理论上距离越近网络延迟可能越低这就是它选择“最优”服务器的逻辑基础。延迟测试与最终选择脚本不会直接选择地理上最近的那个。它会从排序后的列表顶端取出多个例如10个候选服务器并发地向它们发送简单的 HTTP 请求如 GET /speedtest/latency.txt来测量 ping 值。最终它会选择这些候选服务器中延迟最低的那个作为本次测速的目标服务器。执行带宽测试确定了目标服务器后才开始真正的下载和上传带宽测试。脚本会从服务器下载特定大小的数据块计算下载速度然后再向服务器上传数据计算上传速度。2.2 我们遇到的“痛点”具体在哪从流程可以看出问题出在第2步和第3步。自动选择机制对我们来说成了“黑盒”我们失去了控制权。“最优”不等于“目标”地理最近或延迟最低的服务器可能属于不同的运营商比如你用的是电信它给你连到了联通的节点或者根本不是你想监控的那个业务服务器所在的机房。不利于自动化自动化脚本需要稳定、可预期的测试目标。今天自动选的服务器ID是 12345明天这个服务器可能下线了或者列表更新后ID变了脚本就会失败或测到别的服务器上去。列表信息冗余全球服务器列表对我们来说99%是无用的。我们可能只关心固定的几个节点每次都要下载、解析整个巨大列表是一种资源浪费。因此我们的改造思路就是绕过或覆盖原版的自动选择逻辑植入我们自己的服务器指定逻辑。这可以通过多种方式实现比如硬编码、外部配置文件、命令行增强等。3. 改造方案设计从硬编码到配置化根据不同的使用场景和灵活性要求我考虑了三种由简到繁的改造方案。你可以根据自己的需求来选择。3.1 方案一简单硬编码适合固定场景这是最直接粗暴的方法适合测试目标极其固定、且不需要频繁变更的场景。核心思想是在代码中直接替换掉自动选择服务器的函数让它返回我们预设的服务器信息。操作步骤找到服务器选择函数在speedtest-cli的源码中通常是一个叫speedtest.py的文件找到负责挑选服务器的函数。通常是choose_server或get_best_server这类名称的函数。定位服务器列表找到包含服务器详细信息的变量或数据结构。原版是从网络下载的我们可以直接把它替换成一个本地定义好的列表。修改选择逻辑修改choose_server函数。我们可以直接让它忽略距离和延迟计算始终返回我们指定的那个服务器对象。或者在我们本地的小列表里做一个简单的延迟测试后选择如果指定了多个。示例代码片段概念性# 假设原版有一个函数 get_best_server(servers) # 我们将其重写或修改 def get_best_server(servers): # 忽略传入的servers参数使用我们自己的列表 my_servers [ {id: 12345, name: My Beijing Server, host: bj.speedtest.example.com:8080, ...}, {id: 67890, name: My Shanghai Server, host: sh.speedtest.example.com:8080, ...}, ] # 简单起见直接返回第一个或者在这里做一个简单的ping测试 # 这里以返回第一个为例 return my_servers[0]注意硬编码方案的最大缺点是维护性差。服务器信息如host、端口一旦变化就需要重新修改源代码。而且如果你需要测试多个不同服务器就得准备多个修改过的脚本副本。3.2 方案二外部配置文件推荐平衡灵活与简洁这是更优雅和实用的方案。我们将需要指定的服务器信息写在一个独立的配置文件如 JSON、YAML 或 INI 格式里脚本运行时读取这个文件。操作步骤设计配置文件格式例如使用 JSON 格式因为它和原版服务器列表格式接近易于解析。{ servers: [ { id: 1001, name: 北京-电信-节点A, host: speedtest-bj-ct.node-a.com:8080, sponsor: MyCompany }, { id: 1002, name: 上海-联通-节点B, host: speedtest-sh-cu.node-b.com:8080, sponsor: MyCompany } ] }修改脚本以读取配置在speedtest-cli脚本的初始化部分添加读取本地配置文件的逻辑。可以约定一个默认路径如~/.config/speedtest-custom-servers.json或者通过命令行参数--config指定。融合或替换选择逻辑修改服务器选择函数。让它优先使用我们配置文件中的服务器列表。如果配置文件不存在或为空再回退到原版的从网络获取列表的逻辑。在选择时可以如果配置文件中只有一个服务器直接使用它。如果有多个可以在它们之间做一个快速的延迟测试选择最快的但这至少保证了范围是我们指定的。优势服务器信息变更时只需编辑配置文件无需改动脚本。可以轻松管理多组服务器配置通过不同的配置文件。也便于将配置纳入版本控制如 Git。3.3 方案三增强命令行参数最灵活但改动量大这个方案旨在保持与原版命令行参数的兼容性同时增加更强大的指定功能。原版的--server ID需要你先知道ID我们想实现的是通过名称、主机名、标签等更直观的方式来指定。可增强的参数设想--server-host直接通过服务器的主机名host来指定如--server-host bj.speedtest.example.com:8080。--server-name通过服务器名称中的关键字来匹配如--server-name 北京电信。--server-list-file指定一个包含服务器列表的本地文件路径完全替代从网络获取。--no-auto-select强制禁用自动选择必须与上述某一指定参数同时使用。实现难点这需要更深入地重构参数解析逻辑和服务器加载逻辑。你需要修改argparse相关的代码添加新的参数选项并在后续流程中根据这些新参数来决策如何获取和筛选服务器列表。如何选择对于个人使用或小团队方案二外部配置文件是最佳起点它在易用性和维护性上取得了很好的平衡。方案一过于僵化方案三则可能过度设计除非你打算做一个功能更全面的分支版本。4. 实战改造以配置文件方案为例下面我将以方案二外部JSON配置文件为例手把手展示如何对speedtest-cli进行实际修改。我假设你使用的是某个常见版本的speedtest-cliPython源码。4.1 准备工作定位关键代码首先找到你的speedtest-cli脚本文件。它可能是一个单独的speedtest-cli或speedtest.py文件。用文本编辑器打开它。我们需要找到几个关键部分命令行参数解析通常在文件开头看到argparse.ArgumentParser的地方。服务器列表获取函数搜索get_servers、fetch_servers或类似名称的函数。这个函数负责下载和解析那个巨大的服务器列表。服务器选择函数搜索choose_server、get_best_server这个函数接收服务器列表并返回最终选中的那个服务器对象。4.2 第一步添加新的命令行参数在argparse.ArgumentParser的部分添加一个用于指定自定义配置文件的参数。# 在原有的一堆 add_argument 语句附近添加 parser.add_argument( --custom-config, destcustom_config, defaultNone, helpPath to a custom JSON configuration file containing server list. )4.3 第二步创建自定义配置文件读取函数在代码中找一个合适的位置例如在获取服务器列表的函数之前添加一个函数来读取和解析我们的JSON配置文件。import json import os def load_custom_servers(config_path): 从指定的JSON配置文件加载服务器列表 if not config_path or not os.path.exists(config_path): return None try: with open(config_path, r, encodingutf-8) as f: config json.load(f) # 假设配置文件结构是 {servers: [...]} custom_servers config.get(servers, []) if not custom_servers: print(f警告: 配置文件 {config_path} 中未找到有效的 servers 列表。) return None # 这里可能需要将自定义服务器格式转换为与原版内部格式一致 # 原版内部格式可能是一个字典以服务器ID为键 formatted_servers {} for s in custom_servers: # 确保必要的字段存在例如 id, host if id in s and host in s: # 原版可能还需要 url, lat, lon 等字段我们可以提供默认值或从原版逻辑中借用 # 这里简单处理直接存储整个对象 formatted_servers[s[id]] s else: print(f警告: 跳过无效的服务器配置项 {s}) return formatted_servers except json.JSONDecodeError as e: print(f错误: 无法解析配置文件 {config_path}JSON格式错误: {e}) return None except Exception as e: print(f错误: 读取配置文件 {config_path} 时发生未知错误: {e}) return None4.4 第三步修改服务器列表获取逻辑找到原本获取服务器列表的函数假设叫get_servers。修改它使其优先使用自定义配置。def get_servers(self, ...): # ... 原有的部分代码例如获取客户端信息等 ... # 新增尝试加载自定义服务器 if self.config.custom_config: # 假设通过args传递进来的参数保存在self.config中 custom_servers load_custom_servers(self.config.custom_config) if custom_servers: print(f已从自定义配置文件加载 {len(custom_servers)} 个服务器。) # 将自定义服务器列表赋值给 self.servers 或相应的变量 # 注意原版可能期望 servers 是特定格式这里需要适配 # 假设原版 self.servers 是一个以ID为键的字典 self.servers custom_servers return # 直接返回跳过从网络下载 # 如果未使用自定义配置则执行原有的从网络下载列表的逻辑 # ... 原有的下载和解析服务器列表的代码 ...4.5 第四步修改服务器选择逻辑找到choose_server函数。如果我们的自定义服务器列表只有一个服务器我们可以直接返回它跳过延迟测试。如果有多个我们可以保留在原列表中进行延迟测试的逻辑但范围已经限定在我们自定义的这几个里了。def choose_server(self, servers): # servers 现在可能是自定义的服务器列表 if not servers: raise Exception(没有可用的服务器列表。) # 如果只有一个服务器直接返回节省时间 if len(servers) 1: server_id list(servers.keys())[0] print(f仅有一个指定服务器直接选择: {servers[server_id].get(name)}) return servers[server_id] # 如果有多个执行原有的延迟测试逻辑但只在这些服务器中测试 print(f在 {len(servers)} 个指定服务器中进行延迟测试...) # ... 调用原有的 _test_latency 或类似函数但传入我们的 servers ... best self._find_best_server_by_latency(servers) return best4.6 第五步创建并使用配置文件在speedtest-cli脚本所在的目录或者在你的家目录下创建配置文件my_servers.json内容如前面示例。然后运行修改后的脚本python speedtest.py --custom-config ./my_servers.json或者如果你给脚本添加了--server参数并想从自定义列表里选可以这样python speedtest.py --custom-config ./my_servers.json --server 10015. 改造过程中的坑与经验之谈在实际动手修改和后续使用中我踩过不少坑也总结出一些经验希望能帮你绕开弯路。5.1 服务器数据格式的兼容性问题原版speedtest-cli内部使用的服务器对象结构可能比我们想象的复杂。它可能不仅包含id,host,name还有url,latency,d,cc,country,sponsor,url2,preferred等一大堆字段。其中url用于下载测试和host用于延迟测试可能由不同的字段或组合生成。我的经验最稳妥的方法不是自己从头构造而是在原版从网络获取到完整服务器列表后“偷梁换柱”。即先让原版逻辑正常获取一次完整的全球列表然后从中找出我们目标服务器对应的完整对象。我们可以通过host、name或id在完整列表里进行匹配。这样能保证服务器对象包含所有必要的、格式正确的字段避免后续测速步骤因字段缺失或格式错误而崩溃。def get_custom_server_from_full_list(full_servers, custom_host): for server_id, server_info in full_servers.items(): if server_info.get(host) custom_host: # 找到匹配的返回这个完整的服务器对象 return {server_id: server_info} # 没找到可以报错或回退 return None5.2 网络环境与代理的干扰如果你的运行环境需要通过代理访问外网原版speedtest-cli可能因为网络问题无法下载初始的服务器配置文件导致脚本一开始就失败。我们的改造版如果保留了回退到原版下载的逻辑也会遇到同样问题。解决方案离线模式彻底改造使脚本不依赖任何网络下载。将一份“基准”服务器配置文件打包进脚本或者强制要求必须提供--custom-config参数。这适合完全内网或隔离环境。处理代理在脚本中增加对HTTP_PROXY/HTTPS_PROXY环境变量的识别或者在代码中为urllib.request或requests库如果使用显式设置代理。这需要对 Python 的网络请求部分有更深了解。备用下载源修改配置文件的下载 URL指向一个你能稳定访问的镜像或内网地址。5.3 延迟测试方法的差异原版的延迟测试可能不是简单的 ICMP Ping很多服务器禁 Ping而是通过发送一个小的 HTTP GET 请求来计算往返时间。当我们指定服务器时特别是自定义的、非 Ookla 官方的服务器需要确保该服务器提供了相同的延迟测试接口例如在host指定的地址上存在/speedtest/latency.txt这样的路径并能响应。否则延迟测试会失败导致脚本报错。应对策略如果你指定的服务器不是标准的 Speedtest 服务器你可能需要修改延迟测试的逻辑甚至跳过延迟测试步骤直接进行带宽测试如果服务器支持。这需要对测速协议有更深入的了解。5.4 版本升级带来的麻烦我们修改的是特定版本的speedtest-cli源码。当官方发布新版本时其内部函数名、变量结构、网络接口都可能发生变化。直接覆盖升级会导致我们的修改失效。建议将我们的修改以清晰的补丁patch或分支fork的形式管理。记录下修改了哪些文件、哪些函数。如果官方版本更新可以尝试将我们的修改迁移到新版本上或者评估新版本是否已经提供了我们需要的功能虽然目前看来还没有。6. 进阶思路打造属于自己的测速工具链完成基础改造后这个工具的价值才真正开始显现。它可以成为你自动化运维监控体系中的一环。场景一分布式网络质量监控你可以将改造后的脚本部署到全国乃至全球各个关键业务节点服务器、虚拟机、甚至容器内。为每个节点配置一个指向中心监控服务器的配置文件。然后通过定时任务如 crontab定期执行测速并将结果延迟、下载、上传速度上报到时序数据库如 InfluxDB或日志系统。最后用 Grafana 等工具绘制出直观的网络质量地图和趋势图实时掌握各节点到目标机房的链路状态。场景二上线前链路验证在新服务器上架、或进行网络变更如切换 BGP 线路前后运行指定服务器的测速脚本快速验证到核心机房的带宽和延迟是否达到预期标准形成一个简单的验收测试用例。场景三集成到故障排查流程当用户投诉访问慢时可以第一时间从受影响区域的主机运行指向业务服务器的测速脚本快速判断是服务器负载问题还是中间网络链路问题从而缩小排查范围。要实现这些你需要进一步封装脚本。例如编写一个包装脚本接受目标服务器别名作为参数自动选择对应的配置文件运行测速并以 JSON 格式输出结果方便其他程序解析。#!/bin/bash # run_speedtest_to.sh TARGET$1 CONFIG_FILE/etc/speedtest/servers_${TARGET}.json OUTPUT_FILE/var/log/speedtest/$(date %Y%m%d_%H%M%S)_${TARGET}.json python /usr/local/bin/speedtest-custom.py --custom-config $CONFIG_FILE --json $OUTPUT_FILE这个小小的改造从一个简单的需求出发深入到了工具的工作原理、代码结构、配置化设计以及运维实践的结合。它不再只是一个命令的简单调用而成了一个可以根据实际业务需求灵活定制的专业小工具。这种“知其然并知其所以然进而改造之”的过程正是工程师价值的最佳体现。