
搞时间序列的人都知道数据清洗完之后往往还有一道繁琐的工序把异常点、事件区间、换挡点一个个标出来。我之前试过在线标注平台传数据、拉框、加标签界面好看但是有上传大小限制数据敏感的时候也不敢用也试过直接用 Python 脚本画图后手动记录时间一长根本对不上坐标。后来在团队里搭了一个开源的 TimeTagger部署在 Linux 服务器上用 Nginx 做了反向代理整个团队可以直接通过域名访问标注好的结果一键导出喂给模型效率提升非常明显。这篇文章把整个搭建过程中的方案取舍、具体操作和踩过的坑都记录下来给同样需要在本地或服务器上部署时间序列标注工具的朋友做个参考。1. 部署方案选型先把架构理顺再动手1.1 TimeTagger 到底解决了什么问题TimeTagger 是一个面向时间序列数据的可视化标注工具主要用来在二维时间轴上完成三类操作给时间点打标记、给时间段画区间、给每个标记或区间补充标签和备注。对于做异常检测、事件抽取、行为识别的人来说标注数据是模型训练的前置工作工具的核心价值在于导入时间序列数据之后能通过鼠标滚轮缩放、拖拽平移、框选范围快速把关注区域标出来并且把标注结果以结构化的格式导出。我部署的社区版 TimeTagger后端是用 Python Flask 写的前端是 Vue 打包之后的静态资源数据库默认用 SQLite。这个架构对个人和小团队来说足够轻单机部署不需要额外装数据库服务进程管理交给 systemd对外统一由 Nginx 转发整体资源占用很低2 核 4G 的小机器跑起来很稳。如果你的使用场景和这个类似下面的步骤可以完整复刻如果你拿到的是 Docker 镜像或者二进制包思路也一样只是把启动方式换成对应的命令。1.2 手动部署和 Docker 部署怎么选很多工具现在都提供 Docker 部署TimeTagger 也有官方镜像。我不排斥 Docker但在这次场景里选择了手动部署主要考虑三点一是这台 Linux 服务器上还要跑其他标注脚本Docker 的网络隔离会增加调试成本二是标注数据都在本地文件里走 Docker 卷映射反而多一层权限问题三是团队里不止我一个人要访问考虑后续配置 HTTPS、调整 Nginx 缓存和上传大小手动部署更直观。如果你只是自己一个人临时用或者服务器上已经有一套 Docker 体系那直接跑镜像会更省事。但如果你像我一样需要深度定制启动参数、修改静态资源路径、绑定特定 Python 版本手动部署的可控性更强。两种方式的核心区别在于Docker 把进程、依赖和网络都封装在一个容器内适合快速交付手动部署把每个环节都暴露在外面适合长期运维和排障。1.3 外部访问的整体链路设计先说结论我采用的链路是用户浏览器通过域名访问DNS 将域名解析到服务器公网 IP请求进入服务器 80/443 端口由 Nginx 接收Nginx 作为反向代理将请求转发给本机 127.0.0.1:8080 上的 TimeTagger 服务。这里最关键的一点是不要让 TimeTagger 直接监听公网 IP 的端口。原因有三个一是 Flask 内置的开发服务器本身不适合直接暴露在公网并发一高就出现连接抖动二是 Nginx 可以统一处理静态资源、请求日志、SSL 证书和上传大小限制这些功能如果都塞进应用里既难维护又容易出错三是监听回环地址更安全即使外部有扫描器也摸不到应用的真实端口。2. 环境准备先把地基打牢2.1 硬件配置与操作系统要求我用的是 Ubuntu 22.04 LTS如果你用 CentOS/Rocky Linux 或者别的发行版命令可能会有些差异但思路完全一致。硬件方面TimeTagger 本身占用不高标注 100 万行级别的时间序列数据时后端主要是读文件、算时间轴刻度、返回 JSON内存占用大约在 300MB 到 500MBCPU 占用也不大。建议最低配置 1 核 2G正常使用 2 核 4G 比较舒服。如果数据量更大建议把数据文件放在 SSD 上否则缩放的流畅度会明显下降。系统装好之后先把基础工具补齐sudo apt update sudo apt upgrade -y sudo apt install -y python3 python3-venv python3-pip curl wget unzip nginx如果你的服务器上已经装了 Python 3.9 以上版本就不需要重复安装。TimeTagger 依赖的第三方包不多主要是 Flask、Werkzeug 和 Jinja2Python 3.9 到 3.11 都能正常跑。实测在 Python 3.12 下面有个别依赖的版本兼容问题所以如果你能用系统默认 Python 3.10/3.11就尽量别折腾新版本。2.2 创建专用运行用户这是很多初次部署的人容易忽略的一步。直接用 root 用户跑 web 服务会有权限风险万一应用被漏洞利用攻击者拿到的就是 root shell。即便不是安全敏感场景后面做数据备份、日志轮转时用专用用户也更规范。创建一个名叫 timetagger 的用户sudo useradd -r -s /usr/sbin/nologin timetagger sudo mkdir -p /opt/timetagger sudo chown -R timetagger:timetagger /opt/timetagger-r表示创建系统用户-s /usr/sbin/nologin表示禁止该用户登录 shell。后面所有和时间相关的运行操作都会以这个用户的身份执行。数据目录我放在/var/lib/timetagger和程序目录分开这样升级程序时不影响已有数据。2.3 获取 TimeTagger 安装包从项目 GitHub Releases 页面下载最新版 Linux 安装包。以我下载时的版本为例文件是一个 tar.gz 压缩包里面包含app目录、frontend_dist目录、requirements.txt和一个config.example.py。把压缩包放到/opt/timetagger下并解压cd /opt/timetagger sudo -u timetagger tar -xzf timetagger-0.8.3-linux-x86_64.tar.gz -C /opt/timetagger解压之后目录结构大致是这样/opt/timetagger/ ├── app/ ├── frontend_dist/ ├── requirements.txt ├── config.example.py └── run.pyfrontend_dist是编译好的前端静态文件run.py是后端启动入口。这里有个细节不要直接改系统 Python 环境后面所有依赖都装到虚拟环境里这是避免依赖冲突的关键。3. 安装与初始化一步步跑起来3.1 创建虚拟环境并安装依赖进入解压后的目录用 venv 创建虚拟环境cd /opt/timetagger sudo -u timetagger python3 -m venv venv sudo -u timetagger ./venv/bin/pip install --upgrade pip sudo -u timetagger ./venv/bin/pip install -r requirements.txt为什么不直接把依赖装到系统 Python因为你可能还有别的 Python 项目比如时间序列预测模型、数据预处理脚本它们依赖的 Flask 版本可能不同。虚拟环境相当于给 TimeTagger 单独开了一个房间房间里的包版本互不影响。如果你之前踩过“下一个包把上一个包搞崩”的坑应该能明白这一步有多重要。如果安装过程中出现网络超时可能是默认 PyPI 源的问题用国内镜像源加速sudo -u timetagger ./venv/bin/pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 配置数据库路径和监听端口TimeTagger 默认会在运行目录下创建一个 SQLite 数据库文件但更好的做法是放在单独的数据目录方便备份。修改config.py把config.example.py复制出来改核心配置如下HOST 127.0.0.1 PORT 8080 DATA_DIR /var/lib/timetagger SECRET_KEY 换成你自己的随机字符串HOST一定写成127.0.0.1不要写成0.0.0.0。虽然写成0.0.0.0也能从外部访问但这样绕过了 Nginx直接暴露了 Flask 服务会失去日志审计和统一访问控制的能力。SECRET_KEY可以这样生成python3 -c import secrets; print(secrets.token_urlsafe(32))然后把它填到配置文件里。这个密钥用于会话加密如果别人猜到了可能伪造登录状态。3.3 首次启动与验证启动服务验证配置是否正常cd /opt/timetagger sudo -u timetagger ./venv/bin/python run.py如果一切正常终端会显示类似下面的日志* Running on http://127.0.0.1:8080 * Serving Flask app * Debug mode: off注意不要开启 Debug 模式。Flask 的 Debug 模式会在浏览器里提供交互式调试器任何人拿到页面都可以在服务器上执行任意 Python 代码这等于直接打开了后门。我这里特别强调一下生产环境必须保持Debug False。此时在服务器本机验证curl http://127.0.0.1:8080/healthcheck返回OK就说明服务正常。本机访问没问题但还没办法从外部访问下面先把它做成系统服务再配置 Nginx。3.4 用 systemd 把服务托管起来直接终端运行的问题是你一旦关掉 SSH进程就跟着退出。用 systemd 创建一个服务文件让 TimeTagger 在后台常驻、开机自启。新建/etc/systemd/system/timetagger.service[Unit] DescriptionTimeTagger Time Series Annotation Service Afternetwork.target [Service] Typesimple Usertimetagger Grouptimetagger WorkingDirectory/opt/timetagger ExecStart/opt/timetagger/venv/bin/python /opt/timetagger/run.py Restartalways RestartSec3 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.targetRestartalways的意思是无论进程是正常退出还是异常退出都自动拉起。对于标注服务来说偶尔崩溃一次还可以接受但要是没人发现团队那边就会一直打不开页面所以自动重启很重要。配置完成后sudo systemctl daemon-reload sudo systemctl enable --now timetagger sudo systemctl status timetagger看到active (running)就说明服务已经托管好了。以后看日志用journalctl -u timetagger -f非常方便。4. 外部访问配置Nginx 反向代理与 HTTPS4.1 Nginx 配置的核心逻辑Nginx 在这里扮演的是“门卫”角色。外部请求先打到 NginxNginx 判断这个请求是不是合法的 HTTP 请求再转给内部服务。先在/etc/nginx/conf.d/timetagger.conf里写基础代理配置server { listen 80; server_name timetagger.example.com; client_max_body_size 100m; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }client_max_body_size 100m是上传文件大小限制。如果不写这一行Nginx 默认允许 1MB 请求体你导入一个几 MB 的 CSV 文件就会直接报 413。可以根据团队需求调整但是也别调太大否则会占用太多的内存和带宽。proxy_set_header X-Forwarded-Proto $scheme是为了让后端识别原始协议如果后面配了 HTTPS这里必须保留否则应用内部生成的回调链接可能还是 http导致浏览器拦截。改完配置后检查语法并重载sudo nginx -t sudo systemctl reload nginx4.2 防火墙与安全组放行如果你的服务器开启了防火墙只放行 80 和 443 端口。以 ufw 为例sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable如果是云服务器还要去云控制台的安全组里同样放行 80 和 443。这里有一个很容易踩的坑在服务器本地用curl能访问但外部访问不了多半就是云安全组没放行或者云安全组和服务器防火墙的规则冲突。排查顺序是先看云安全组再看服务器防火墙最后看 Nginx 监听端口。4.3 配置 HTTPS 证书外部访问如果还停留在明文 HTTP数据在链路上可以被截获。标注的数据集本身可能包含业务敏感信息所以 HTTPS 不能省。用 Certbot 免费证书是最直接的方式sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d timetagger.example.comCertbot 会自动修改 Nginx 配置把 80 端口的请求重定向到 443并配置好证书路径。证书有效期一般是 90 天Certbot 会写入一个 systemd timer 自动续期。验证自动续期任务是否存在sudo systemctl list-timers | grep certbot如果看到定时器就不用管了。如果没看到可以手动增加 crontab0 3 * * * /usr/bin/certbot renew --quiet配置完成后浏览器访问https://timetagger.example.com地址栏会出现小锁图标。此时外部访问链路已经完整。4.4 多用户访问的权限建议TimeTagger 默认支持多用户登录吗不同版本行为不一样但大多数社区版是单用户或者简单共享模式。如果你要给团队多人使用建议在 Nginx 层加一层 HTTP Basic Auth先做一道访问门槛。生成密码文件sudo apt install -y apache2-utils sudo htpasswd -c /etc/nginx/.timetagger_htpasswd team然后在 Nginx 的 server 块中加入location / { auth_basic Restricted; auth_basic_user_file /etc/nginx/.timetagger_htpasswd; ... }这样即使有人扫到了域名也是先看到浏览器弹出的登录框拿不到凭据就不会接触标注页面。当然这只是临时方案更严谨的做法是在应用层面接入 LDAP 或 OAuth需要看具体版本的支持情况。5. 常见问题与排查实录5.1 502 Bad Gateway出现 502说明 Nginx 已经收到请求但后端服务没响应。排查顺序systemctl status timetagger journalctl -u timetagger -n 50大概率是后端进程挂了。看日志里有没有报错比如数据库文件不可写、端口被占用。还有一个小概率原因Nginx 配置里proxy_pass写成了http://localhost:8080而服务只监听127.0.0.1在某些系统上 localhost 解析成 IPv6 的::1和后端监听的 IPv4 地址对不上也会 502。统一写成http://127.0.0.1:8080即可。5.2 外部访问不了本机却正常这是最常见的网络问题。先用ss -lntp | grep 8080查看服务是否只监听在 127.0.0.1 上再用ss -lntp | grep 443看 Nginx 是否监听 0.0.0.0。如果服务器有多个网卡Nginx 的listen需要写8080端口对外Nginx 默认监听所有 IPv4 地址一般没问题。接下来逐层检查curl http://127.0.0.1:8080/healthcheck curl -H Host: timetagger.example.com http://127.0.0.1/第二个命令直接请求本机的 Nginx并模拟域名访问。如果返回正常说明 Nginx 和后端链路通问题就出在外部入口。再去检查防火墙和安全组。还有一个细节是 SELinux。如果系统是 CentOS 且开启了 SELinuxNginx 默认不允许向本地端口发起网络连接。需要执行sudo setsebool -P httpd_can_network_connect 1或者直接查看/var/log/audit/audit.log里是否有 denied 记录。我见过多个部署案例卡在这里日志不报错但就是连不上。5.3 数据上传失败或标注保存不了标注结果保存要写 SQLite 数据库。SQLite 对文件权限很敏感如果进程用户没有数据目录的写权限会出现database is locked或unable to open database file的报错。解决方法是确认数据目录属主sudo chown -R timetagger:timetagger /var/lib/timetagger如果数据文件没问题再看磁盘空间df -h查看空间是否满了。SQLite 在磁盘写满时不会立刻崩溃但会返回奇怪的错误比如disk I/O error。上传大文件时还要检查 Nginx 的client_max_body_size和后端框架的上传限制两边都要调。5.4 浏览器访问是白的或 JS 静态资源加载失败页面能打开但功能全无打开开发者工具会发现frontend_dist下的 JS/CSS 文件返回 404 或 502。这通常是前端静态文件没有正确部署或者 Nginx 没有把静态资源请求交给正确的目录。如果 TimeTagger 的架构是前后端分离你需要把frontend_dist路径暴露给 Nginx例如location /static/ { alias /opt/timetagger/frontend_dist/; }具体路径要看实际版本。如果后端能正确处理静态文件就不需要额外配这条。判断方法是看日志里的请求路径是/static/xxx.js还是/assets/xxx.js再对应调整。再分享几个小经验数据库文件建议每天做一次定时备份因为标注工作一旦丢失后悔都来不及。备份命令其实很简单SQLite 支持在线备份写一个定时任务0 2 * * * sqlite3 /var/lib/timetagger/data.db .backup /var/backups/timetagger/data_$(date %F).db备份文件保留最近七天就够。升级 TimeTagger 之前先停服务、备份数据、再解压新版本不要直接在旧目录上覆盖否则配置文件可能被新版本重置。我试过直接覆盖结果自定义端口和 SECRET_KEY 全丢了服务起不来白白折腾了半小时。另外如果你打算把部署过程写成团队文档别只写命令最好把配置文件、服务文件和 Nginx 站点配置都 git 一下放到内部代码仓库。这样哪一天服务器重装了按文档十分钟就能恢复。对我来说最简单的幸福感就是数据没丢、服务没崩、团队能正常访问。剩下的优化等你真正用起来再说。