Ubuntu 14.04 上稳定部署 Bottle Web 服务实战指南
1. 项目概述:为什么在 Ubuntu 14.04 上部署 Bottle 应用至今仍有现实价值
你点开这个标题,第一反应可能是:“Ubuntu 14.04?那不是2014年发布的系统吗?早该淘汰了吧?”——这恰恰是我要先破掉的第一个认知误区。没错,Ubuntu 14.04 的官方标准支持(LTS)确实在2019年4月就结束了,但它的长期技术惯性和特定场景下的不可替代性,至今仍在真实世界中持续运转。我过去三年里参与的7个边缘计算节点、老旧工控网关、嵌入式教学实验箱、以及某高校物联网实验室的传感器数据聚合平台,底层操作系统清一色仍是 Ubuntu 14.04。不是因为运维懒,而是因为:这些设备出厂固件锁定内核版本,升级会直接导致USB串口驱动失效;某些定制硬件的BSP包只适配到 kernel 3.13;还有些老教师坚持用“能跑通就行”的教学镜像,拒绝任何可能打乱课堂节奏的变更。
Bottle 框架在这个语境下,就不是“又一个轻量Web框架”的泛泛之选,而是一个精准匹配资源约束与开发效率的工程解。它单文件、无依赖、零配置的特性,意味着你不需要在内存仅512MB、硬盘只有4GB的树莓派Zero W上折腾 virtualenv 和 pip 版本冲突;也不用担心 Flask 的 Jinja2 模板引擎在 Python 2.7.6(Ubuntu 14.04 默认版本)下因 Unicode 处理差异引发的中文乱码;更不必为 Django 的 manage.py 启动时多消耗的80MB内存发愁。Bottle 的核心逻辑就藏在bottle.py这一个不到2000行的文件里——我把它拖进/usr/local/lib/python2.7/dist-packages/后,连pip install bottle都可以跳过。这种“所见即所得”的确定性,在老旧环境里比任何新潮特性都珍贵。
所以,这不是一篇怀旧考古文,而是一份面向真实运维现场的生存指南。它解决的核心问题很朴素:如何让一个用 Python 写的、功能明确的小型 Web 服务(比如设备状态看板、API 数据中转、配置下发接口),在一台既不能重装系统、又不敢轻易升级内核、还常年离线的 Ubuntu 14.04 机器上,稳定、安静、不占资源地跑起来,并且能被局域网里的其他设备可靠访问。它适合三类人:嵌入式/工控领域的 Python 初学者(你不需要懂 Nginx 反向代理原理,但得知道怎么让浏览器输入 IP 就看到页面);高校实验室管理员(你手头有20台预装14.04的树莓派,学生交来的 Bottle 代码必须能一键部署);以及那些还在维护十年前遗留系统的“救火队员”(老板说“这个页面不能挂,但服务器别动”)。接下来的所有步骤,我都基于真实复现——在 VirtualBox 里克隆了纯净的 Ubuntu 14.04.6 Server 镜像,从apt-get update开始,全程录屏验证,连sudo service nginx restart后的返回码都截图存档。没有“理论上可行”,只有“我亲手敲过,它活了”。
2. 整体架构设计:为什么放弃 Gunicorn + Nginx 组合,选择 Bottle 自带服务器 + Supervisor
部署 Python Web 应用,行业默认路径往往是“Gunicorn + Nginx”。但当你把这套组合硬塞进 Ubuntu 14.04 的老旧躯壳里,会立刻撞上三堵墙:第一堵是Python 版本墙。Ubuntu 14.04 自带 Python 2.7.6,而 Gunicorn 20.0+ 要求 Python 3.5+,降级到 Gunicorn 19.10 是唯一选择,但它对asyncio的兼容性极差,一旦你的 Bottle 应用里用了gevent或eventlet做异步,启动就报ImportError: No module named asyncio;第二堵是依赖墙。Nginx 在 14.04 的 apt 源里版本是 1.4.6,它不支持stream模块,无法做 TCP 层负载均衡;而手动编译新版 Nginx,又需要先装build-essential、libpcre3-dev、libssl-dev,这些包在离线环境中就是天堑;第三堵是资源墙。Gunicorn 工作进程默认吃掉 30MB 内存,Nginx 主进程加工作进程再吃 25MB,对于总内存 1GB 的设备,留给业务逻辑的只剩 400MB,而 Bottle 本身启动一个进程才 8MB。
我的方案是:彻底放弃应用服务器与反向代理的分层架构,回归 Bottle 的原始能力——用其内置的wsgi服务器作为生产环境主力,再用 Supervisor 管理其生命周期。这听起来像倒退,实则是精准的外科手术。Bottle 的run()函数底层调用的是wsgiref.simple_server.make_server,这是一个纯 Python 实现的 HTTP 服务器,它不依赖 C 扩展,不引入额外动态链接库,完美兼容 Python 2.7.6 的所有 ABI。更重要的是,它支持server='paste'、server='cherrypy'等插件模式,而paste服务器(通过pip install paste安装)在 14.04 上能稳定处理 50+ 并发连接,且内存占用恒定在 12MB 左右——这是我用ps aux --sort=-%mem | head -5在压力测试中反复确认的数据。
Supervisor 的选型则源于其“不挑食”的特性。Ubuntu 14.04 的 apt 源里自带supervisor3.0b2 版本,它不依赖 systemd(14.04 用的是 Upstart),配置文件语法简单到只有[program:myapp]、command=、autostart=true三行核心指令。最关键的是,Supervisor 的进程守护机制是 fork+waitpid,它不扫描/proc下的线程数,不会因为 Bottle 应用内部创建了子进程(比如调用subprocess.Popen执行 shell 脚本)而误判主进程已死。我曾见过用systemd管理 Bottle 的案例,当应用需要调用ffmpeg转码视频时,systemd把 ffmpeg 子进程当成僵尸进程干掉,导致整个服务崩溃。Supervisor 不会犯这种错。
这个架构的物理拓扑极其清晰:用户浏览器 → 直接访问http://192.168.1.100:8080→ Ubuntu 14.04 的 8080 端口 → Supervisor 启动的bottle.py进程 →paste服务器接收请求 → Bottle 路由分发 → 返回 HTML 或 JSON。没有中间商赚差价,没有协议转换损耗,没有配置文件嵌套地狱。它牺牲了 Nginx 的静态文件缓存、SSL 终止、限流等高级功能,但换来了在资源受限环境下的绝对可控性。如果你的应用压根不需要 HTTPS(比如内网设备管理页),或者静态文件少于 10 个(CSS/JS/图片),这个方案的稳定性远超“标准答案”。
3. 核心细节解析:Bottle 应用编写、Paste 服务器配置与 Supervisor 守护的实操要点
3.1 Bottle 应用代码的“14.04 友好写法”
一个看似简单的hello world,在 Ubuntu 14.04 上可能埋着三个坑。我以一个真实的设备状态监控应用为例,展示关键代码段及其避坑逻辑:
# app.py from bottle import Bottle, run, static_file, request, response import os import json import sys # 【坑1:编码声明】Ubuntu 14.04 的 locale 默认是 'C',不支持 UTF-8 文件名 # 必须显式设置,否则 os.listdir() 读取含中文路径会报 UnicodeDecodeError reload(sys) sys.setdefaultencoding('utf-8') app = Bottle() # 【坑2:静态文件路由】不要用 bottle 的 default static_file,它在 2.7.6 下对路径拼接有 bug @app.route('/static/<filename:path>') def server_static(filename): # 手动拼接绝对路径,避免 ../ 路径遍历漏洞 safe_path = os.path.normpath(os.path.join(os.getcwd(), 'static', filename)) if not safe_path.startswith(os.path.join(os.getcwd(), 'static')): return "Forbidden" return static_file(filename, root='./static') # 【坑3:JSON 响应】bottle 0.12.13(14.04 pip 最高版)的 json_dumps 不处理 datetime @app.route('/api/status') def get_status(): data = { 'device_id': 'ESP32-001', 'uptime': '2 days, 5:32:18', 'temperature': 23.5, 'timestamp': '2024-06-15T14:22:30' # 用字符串代替 datetime 对象 } # 显式设置 Content-Type,避免 bottle 自动推断出 text/html response.content_type = 'application/json; charset=utf-8' return json.dumps(data, ensure_ascii=False) # 主页路由,返回 index.html @app.route('/') def home(): return static_file('index.html', root='./static') # 关键:禁用 bottle 的自动重载和调试模式,生产环境必须关闭 if __name__ == '__main__': # 这里不直接 run(),留给 supervisor 控制 pass这段代码里藏着三个必须手动处理的细节。第一处reload(sys)和setdefaultencoding,是 Python 2.7 的历史包袱。Ubuntu 14.04 的locale -a | grep zh_CN输出为空,locale.getpreferredencoding()返回'ANSI_X3.4-1968',这会导致任何涉及中文路径的操作失败。第二处静态文件路由,bottle.static_file在 0.12.13 版本中对root参数的路径规范化有缺陷,../etc/passwd这样的恶意路径可能绕过检查,所以必须用os.path.normpath和startswith做双重校验。第三处 JSON 响应,bottle.json_dumps在旧版本里不支持default参数,无法序列化datetime,强行传入会抛TypeError,所以统一用字符串时间戳。这些都不是文档里会写的“最佳实践”,而是我在某次凌晨三点排查设备页面空白时,用strace -e trace=open python app.py抓到的系统调用错误日志里挖出来的真相。
3.2 Paste 服务器的安装与参数调优
Bottle 内置的wsgiref服务器只能用于开发,生产环境必须换。paste是最稳妥的选择,但它的安装和配置有门道:
# Ubuntu 14.04 的 pip 是 1.5.4,不支持 --pre 参数,必须用 easy_install sudo apt-get install python-setuptools sudo easy_install paste # 验证安装 python -c "from paste import httpserver; print(httpserver.__version__)" # 输出应为 2.0.2(这是 14.04 兼容的最高版)paste的核心配置不在 Python 代码里,而在独立的.ini文件中。创建production.ini:
[server:main] use = egg:Paste#http host = 0.0.0.0 port = 8080 # 【关键参数1:threads】默认是 5,但 14.04 的调度器在高并发下容易饿死 # 实测 12 线程是内存与性能的平衡点(1GB 内存下) threads = 12 # 【关键参数2:socket_timeout】默认 300 秒,但老旧网络常有长连接假死 # 设为 60 秒,让空闲连接更快释放 socket_timeout = 60 # 【关键参数3:request_queue_size】默认 5,队列太小会导致请求被丢弃 # 设为 20,缓冲突发流量 request_queue_size = 20 [composite:main] use = egg:Paste#urlmap / = myapp [app:myapp] paste.app_factory = app:app这个.ini文件的每一行都是血泪教训。threads = 12是我用ab -n 1000 -c 50 http://127.0.0.1:8080/api/status压测后确定的:设为 20,内存占用飙升至 45MB,CPU 占用率超过 90%,响应延迟从 12ms 涨到 200ms;设为 8,QPS(每秒查询率)从 320 掉到 180,大量请求超时。socket_timeout = 60则源于一次现场故障:某台工控机的网卡驱动 Bug 导致 TCP 连接不发 FIN 包,paste服务器一直维持着 200 个“半开”连接,新请求全部排队等待,最终 Supervisor 因超时判定进程死亡并重启,造成服务雪崩。request_queue_size = 20是为了应对设备批量上报数据的瞬时高峰——我们实验室的温湿度传感器每分钟同步一次,30 台设备在 1 秒内集中请求,队列太小就会直接Connection refused。
启动命令不再是python app.py,而是:
# 在项目根目录执行 paster serve production.ini --daemon # --daemon 让它后台运行,但此时还没被 supervisor 管理3.3 Supervisor 配置的“防自杀”机制
Supervisor 的配置文件/etc/supervisor/conf.d/bottle-app.conf看似简单,但两处设置决定了服务能否真正“不死”:
[program:bottle-app] # 【关键1:command 路径必须绝对】相对路径在 supervisor 启动时会失效 command=/usr/bin/paster serve /home/pi/myapp/production.ini # 【关键2:directory 必须指定,否则 static_file 找不到文件】 directory=/home/pi/myapp user=pi autostart=true autorestart=true # 【关键3:startsecs 必须 > 1】默认是 1,但 paste 启动有延迟 startsecs=3 # 【关键4:stopwaitsecs 必须足够长】paste 正常关闭需 2 秒以上 stopwaitsecs=5 # 【关键5:redirect_stderr=true】否则日志全丢进黑洞 redirect_stderr=true stdout_logfile=/var/log/bottle-app.log # 【关键6:environment】显式声明 PYTHONPATH,避免模块导入失败 environment=PYTHONPATH="/home/pi/myapp"这里startsecs=3和stopwaitsecs=5是最容易被忽略的致命点。paster serve启动时,paste服务器要绑定端口、初始化线程池、加载 Bottle 应用,整个过程在 14.04 上平均耗时 2.3 秒。如果startsecs=1,Supervisor 会在服务器还没准备好时就判定启动失败,然后疯狂重启,形成“启动-失败-重启”的死亡循环。stopwaitsecs=5同理:paster收到SIGTERM后,会优雅地等待所有活跃请求完成再退出,这个过程在高负载下可能长达 4.8 秒。设成 3 秒,Supervisor 会直接SIGKILL强杀,导致连接中断、数据丢失。我曾经因此丢失过一批传感器的校准参数,最后靠翻查/var/log/syslog里 supervisor 的KILLED日志才定位到问题。
日志路径stdout_logfile必须用绝对路径,且确保pi用户有写权限。我习惯在部署前执行:
sudo mkdir -p /var/log/bottle-app sudo chown pi:pi /var/log/bottle-app这样,当应用出错时,tail -f /var/log/bottle-app.log就能看到最真实的 traceback,而不是在/var/log/supervisor/supervisord.log里找一堆spawn error的模糊提示。
4. 实操全流程:从零开始部署一个可访问的 Bottle 设备管理页面
4.1 环境初始化与依赖安装(全程离线可复现)
假设你拿到一台全新的 Ubuntu 14.04.6 Server(Minimal Install),IP 为192.168.1.100,SSH 已启用。以下是我在虚拟机里逐字敲入、耗时 4 分钟完成的完整操作链:
# 步骤1:更新源并安装基础工具(14.04 的源已归档,需切换) sudo sed -i 's/archive.ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list sudo sed -i 's/security.ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list sudo apt-get update # 步骤2:安装 Python 开发环境(14.04 默认不装 python-dev) sudo apt-get install -y python-dev python-pip python-setuptools # 步骤3:升级 pip 到兼容版本(14.04 的 pip 1.5.4 太老,但不能升太高) # 使用 get-pip.py 安装 pip 9.0.3(最后一个支持 Python 2.7.6 的稳定版) curl https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py sudo python get-pip.py sudo pip install --upgrade pip==9.0.3 # 步骤4:安装 bottle 和 paste(注意版本锁) sudo pip install bottle==0.12.13 sudo easy_install paste==2.0.2 # 步骤5:安装 supervisor(14.04 源里有,直接装) sudo apt-get install -y supervisor # 步骤6:创建项目目录并赋权 sudo mkdir -p /home/pi/myapp/{static,logs} sudo chown -R pi:pi /home/pi/myapp这个流程的关键在于源地址切换和pip 版本锁定。old-releases.ubuntu.com是 Ubuntu 官方为 EOL 版本维护的归档源,不切换的话apt-get update会直接失败。而pip升级到 9.0.3 是必须的——旧版 pip 在安装bottle时会尝试下载wheel格式包,但 14.04 的wheel包不兼容,导致pip install bottle卡死在Downloading ...。get-pip.py是官方推荐的离线升级方式,它不依赖网络源,直接下载预编译的.whl。
4.2 应用代码与配置文件的创建(含完整可运行示例)
在/home/pi/myapp/下创建以下文件:
app.py(设备状态页核心逻辑):
from bottle import Bottle, route, static_file, request, response import os import json import time app = Bottle() @app.route('/') def index(): return static_file('index.html', root='./static') @app.route('/static/<filename:path>') def server_static(filename): safe_path = os.path.normpath(os.path.join(os.getcwd(), 'static', filename)) if not safe_path.startswith(os.path.join(os.getcwd(), 'static')): return "Forbidden" return static_file(filename, root='./static') @app.route('/api/device') def device_info(): # 模拟读取设备信息(实际可替换为串口通信或文件读取) info = { "model": "ESP32-WROOM-32", "firmware_version": "v2.1.4", "last_update": time.strftime("%Y-%m-%d %H:%M:%S"), "memory_usage": "42%", "uptime": "1 day, 8:45:22" } response.content_type = 'application/json; charset=utf-8' return json.dumps(info, ensure_ascii=False) @app.route('/api/control', method='POST') def control_device(): try: data = request.json if not data or 'command' not in data: raise ValueError("Missing command") # 这里写实际控制逻辑,比如调用 subprocess 执行 shell 脚本 response.status = 200 return {"status": "success", "command": data['command']} except Exception as e: response.status = 400 return {"error": str(e)}static/index.html(前端页面,纯静态):
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ESP32 设备管理</title> <style> body { font-family: Arial, sans-serif; margin: 40px; } .card { border: 1px solid #ccc; padding: 15px; margin: 10px 0; } button { background: #4CAF50; color: white; border: none; padding: 10px 20px; } </style> </head> <body> <h1>ESP32 设备管理面板</h1> <div class="card" id="info"></div> <div class="card"> <h3>控制指令</h3> <button onclick="sendCommand('reboot')">重启设备</button> <button onclick="sendCommand('update')">固件升级</button> </div> <script> function loadInfo() { fetch('/api/device') .then(r => r.json()) .then(data => { document.getElementById('info').innerHTML = `<h3>设备信息</h3> <p><strong>型号:</strong>${data.model}</p> <p><strong>固件版本:</strong>${data.firmware_version}</p> <p><strong>最后更新:</strong>${data.last_update}</p> <p><strong>内存使用:</strong>${data.memory_usage}</p> <p><strong>运行时间:</strong>${data.uptime}</p>`; }); } function sendCommand(cmd) { fetch('/api/control', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({command: cmd}) }) .then(r => r.json()) .then(data => alert('指令已发送:' + data.command)); } loadInfo(); setInterval(loadInfo, 5000); // 每5秒刷新一次 </script> </body> </html>production.ini(Paste 服务器配置):
[server:main] use = egg:Paste#http host = 0.0.0.0 port = 8080 threads = 12 socket_timeout = 60 request_queue_size = 20 [composite:main] use = egg:Paste#urlmap / = myapp [app:myapp] paste.app_factory = app:app/etc/supervisor/conf.d/bottle-app.conf(Supervisor 配置):
[program:bottle-app] command=/usr/bin/paster serve /home/pi/myapp/production.ini directory=/home/pi/myapp user=pi autostart=true autorestart=true startsecs=3 stopwaitsecs=5 redirect_stderr=true stdout_logfile=/var/log/bottle-app.log environment=PYTHONPATH="/home/pi/myapp"4.3 启动与验证:三步确认服务真正可用
配置完成后,执行以下三步,每一步都有明确的验证信号:
第一步:重载 Supervisor 配置
sudo supervisorctl reread # 输出应为:bottle-app: available sudo supervisorctl update # 输出应为:bottle-app: added process group如果reread输出Error: The file /etc/supervisor/conf.d/bottle-app.conf has no section,说明配置文件语法错误,常见于漏写[program:bottle-app]方括号。
第二步:启动服务并检查状态
sudo supervisorctl start bottle-app # 输出应为:bottle-app: started sudo supervisorctl status # 输出应为:bottle-app RUNNING pid 1234, uptime 0:00:05如果状态是STARTING或FATAL,立刻执行sudo tail -n 20 /var/log/supervisor/supervisord.log查看错误。最常见的FATAL原因是command路径错误或directory权限不足。
第三步:本地与远程访问验证
# 在服务器本机 curl 测试 curl -I http://127.0.0.1:8080 # 应返回 HTTP/1.0 200 OK # 在另一台电脑浏览器访问 # 输入 http://192.168.1.100:8080 # 应看到完整的设备管理页面,且 F12 控制台无 404 错误 # 点击“重启设备”按钮,应弹出“指令已发送:reboot”提示框最关键的验证点是浏览器访问。很多教程只教curl测试,但curl成功不代表浏览器能打开——因为curl不加载 CSS/JS,而我们的index.html里引用了static/style.css(虽然没写,但路径存在)。如果浏览器打开是白屏,F12看 Network 标签,90% 的概率是static/index.html返回了 403 Forbidden,根源就是app.py里safe_path.startswith(...)校验失败,说明directory配置或static_file路径写错了。
5. 常见问题与排查技巧实录:那些文档里绝不会写的“踩坑现场”
5.1 “Connection Refused” 的五种真实原因与速查表
当curl http://192.168.1.100:8080返回curl: (7) Failed to connect to 192.168.1.100 port 8080: Connection refused,新手常以为是端口没开,其实背后有五个完全不同的技术场景:
| 现象特征 | 根本原因 | 排查命令 | 解决方案 |
|---|---|---|---|
sudo netstat -tuln | grep :8080无输出 | Paste 服务器根本没启动 | sudo supervisorctl status | 检查supervisorctl start是否执行,看status输出是否为RUNNING |
netstat显示127.0.0.1:8080而非*:8080 | production.ini中host = 127.0.0.1写死了 | grep host /home/pi/myapp/production.ini | 改为host = 0.0.0.0,sudo supervisorctl restart bottle-app |
netstat显示*:8080,但telnet 192.168.1.100 8080连不上 | Ubuntu 防火墙 ufw 阻断 | sudo ufw status verbose | sudo ufw allow 8080,或sudo ufw disable(内网环境推荐) |
telnet能连上但立即断开 | Paste 服务器启动失败,Supervisor 未捕获 | sudo tail -n 50 /var/log/bottle-app.log | 查看日志末尾是否有ImportError或SyntaxError,常见于app.py第 3 行少了个冒号 |
telnet连上后卡住不动 | socket_timeout设置过大,服务器假死 | sudo lsof -i :8080看 ESTABLISHED 连接数 | 降低socket_timeout到 30,重启服务 |
我遇到最诡异的一次是第五种:lsof显示 200+ 个ESTABLISHED连接,但ps aux \| grep paster只看到一个进程。用strace -p $(pgrep paster)发现它卡在accept()系统调用上,原因是request_queue_size设为 5,而客户端(某款国产工控软件)的 HTTP 客户端 Bug 导致它不遵守Connection: close,疯狂复用连接。解决方案不是改服务器,而是给客户端加-H "Connection: close"头,这属于跨团队协作的灰色地带,但现场必须有人扛下来。
5.2 “500 Internal Server Error” 的日志深挖法
浏览器打开页面显示 500 错误,supervisor状态却是RUNNING,这是最折磨人的场景。此时supervisor的日志里只有spawned,真正的错误藏在 Bottle 应用自己的日志里。正确做法是:
强制 Bottle 输出详细错误:在
app.py开头添加:from bottle import debug debug(True) # 开启调试模式,错误信息直接返回浏览器重新启动服务,浏览器就能看到完整的
Traceback。注意:这只能临时开启,生产环境必须关掉,否则泄露服务器路径。捕获未处理异常:Bottle 默认不记录未捕获的异常。在
app.py底部加全局错误处理器:@app.error(500) def error500(error): import traceback, logging logging.error("500 Error: %s", traceback.format_exc()) return "Server Error"检查 Python 模块路径:最常见的 500 是
ImportError。比如你写了import serial,但没装pyserial。sudo pip install pyserial后,必须重启 Supervisor:sudo supervisorctl restart bottle-app。很多人装完就以为好了,忘了重启,因为supervisor启动时已加载了旧的 Python 环境。
5.3 Supervisor 的“静默失败”急救包
Supervisor 有个阴险特性:当command命令执行后立即退出(比如脚本有语法错误),它不会报错,而是不断重启,日志里只有spawned和exited。这时你需要:
- 查看子进程 PID:
sudo supervisorctl status显示pid 1234,但ps aux \| grep 1234找不到进程,说明它启动即死。 - 手动模拟启动:
sudo -u pi /usr/bin/paster serve /home/pi/myapp/production.ini,此时终端会直接打印错误,比如SyntaxError: invalid syntax。 - 检查环境变量:Supervisor 启动的进程不继承用户的
~/.bashrc,所以PYTHONPATH必须在配置文件里显式声明,否则import app会失败。
最后分享一个独门技巧:在app.py开头加一行print("App starting at %s" % time.time()),然后sudo tail -f /var/log/bottle-app.log。如果日志里只有一行App starting at ...就停住,说明卡在某个阻塞操作上(比如serial.Serial('/dev/ttyUSB0')等待设备响应)。这时候就要祭出strace了——这才是老运维的真功夫。
6. 后续演进与安全加固:从能跑到稳跑的必经之路
当你的 Bottle 应用在 Ubuntu 14.04 上稳定运行一周后,下一步不是追求新功能,而是做两件事:最小化攻击面和建立可观测性。前者让你的设备不被扫到,后者让你在出问题时 30 秒内定位。
6.1 端口与网络层加固:让服务“隐身”
Bottle 默认监听0.0.0.0:8080,这意味着它接受来自任何 IP 的连接。在工控或实验室场景,这等于把大门敞开。加固只需三步:
绑定到内网 IP:修改
production.ini:[server:main] host = 192.168.1.100 # 不再用 0.0.0.0这样只有同一局域网的设备能访问,外网路由器 NAT 过来的请求会被直接拒绝。
用 iptables 限制来源:即使绑定了内网 IP,也要防内网扫描。添加规则只允许特定 IP 段:
sudo iptables -A INPUT -p tcp --dport 8080 -s 192.168.1.0/24 -j ACCEPT sudo iptables -A INPUT -p tcp --dport 8080 -j DROP sudo iptables-save | sudo tee /etc/iptables/rules.v4这条规则的意思是:只允许
192.168.1.x的设备访问 8080 端口,其他一律丢弃。iptables-save确保重启后规则不丢失。禁用不必要的 HTTP 方法:Bottle 默认允许所有方法,但你的 API 可能只需要
GET和POST。在app.py中添加:from bottle import hook @hook('before_request') def validate_method(): if request.method not in ['GET', 'POST']: abort(405
