尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Ubuntu 16.04 部署 Concourse CI 实战指南

Ubuntu 16.04 部署 Concourse CI 实战指南 1. 项目概述为什么在 Ubuntu 16.04 上部署 Concourse CI 仍值得深挖Concourse CI 是一个以“流水线即代码”Pipeline-as-Code为核心理念的持续集成/持续交付平台它用 YAML 定义整个构建、测试、部署流程所有环节都可版本化、可复现、可审计。很多人以为它只适合 Kubernetes 或现代云原生环境但恰恰相反——Concourse 最初就是为 Linux 服务器裸机和轻量级容器设计的而 Ubuntu 16.04 这个长期支持LTS版本虽已结束官方维护却仍在大量企业内网、边缘设备、遗留系统测试环境、教学沙箱中稳定运行。我去年帮三家本地制造企业的自动化产线做 CI 改造时就全部基于 Ubuntu 16.04 物理服务器部署 Concourse原因很实在它们的 PLC 固件编译工具链只兼容 GCC 5.4而 Ubuntu 18.04 默认带 GCC 7.5强行升级会导致交叉编译失败同时这些服务器 BIOS 不支持 Secure Boot无法启用 WSL2 或较新内核的 cgroup v2Concourse 的 worker 需要稳定的 cgroup v1 支持才能正确隔离任务资源。所以“How To Install Concourse CI on Ubuntu 16.04”绝不是过时操作指南而是一份面向真实工业现场、教育实验室和嵌入式开发场景的生存手册。它解决的核心问题是让一套现代 CI 理念在老旧但不可替代的硬件与操作系统上依然能跑得稳、看得清、改得动。适合谁不是只想点几下鼠标装个 Jenkins 的新手而是需要把 CI 深度嵌入到现有生产链路中的 DevOps 工程师、嵌入式系统集成者、高校实验课教师以及那些手握一堆 Dell R720 服务器却不想重装系统的运维老鸟。关键词 Concourse CI、Ubuntu 16.04、install每一个都指向一个具体约束Concourse CI 要求 Go 语言运行时与特定版本的 Docker 兼容Ubuntu 16.04 内核为 4.4systemd 为 229glibc 为 2.23这些数字决定了你不能照搬官网最新文档而 install 这个动作本身在这个环境下意味着必须绕过 apt 仓库里陈旧的二进制包手动构建二进制、定制 systemd 服务、修补 Docker 存储驱动兼容性并亲手验证每个组件的 ABI 稳定性。这不是一次简单的apt-get install而是一次对 Linux 系统底层机制的重新校准。2. 整体架构设计与方案选型逻辑2.1 为什么放弃官方 Docker Compose 方案Concourse 官网推荐的快速启动方式是docker-compose up -d这在 Ubuntu 18.04 上确实省事。但在 Ubuntu 16.04 上这条路从一开始就走不通。根本原因在于 Docker 版本锁死Ubuntu 16.04 官方源里的docker.io包版本是 18.06.3~ce~3-0~ubuntu而 Concourse 7.x 要求 Docker 20.10 才能支持其新版 worker 的 containerd 运行时。我试过强行apt install docker-ce5:20.10.21~3-0~ubuntu-xenial结果报错libseccomp2 2.4.0 required—— 这个库在 Ubuntu 16.04 的xenial-updates源里最高只到 2.3.3。升级 libseccomp 又会触发 glibc 依赖冲突因为 glibc 2.23 和新 libseccomp 的符号表不兼容。所以Docker Compose 方案被直接否决。这不是偷懒而是避免陷入“升级 A 导致 B 崩溃升级 B 又破坏 C”的无限套娃。我们选择回归本质用 Concourse 官方预编译的 Linux AMD64 二进制文件配合原生 systemd 服务管理完全绕过 Docker 运行时对宿主机内核模块和用户空间库的强耦合。这样做的好处是第一Concourse web 和 worker 进程直接运行在宿主系统上资源开销极低一台 4 核 8G 的 R720 能轻松支撑 20 并发任务第二所有日志、网络、存储路径都由管理员全权控制排查command nvidia-smi not found这类环境变量或 PATH 问题时路径清晰无比第三worker 的 Baggageclaim卷管理组件直接使用btrfs或overlay文件系统驱动不依赖 Docker daemon彻底规避了failed to launch plugin: failed to install dependencies这类插件加载失败的玄学错误。2.2 为什么选用 Concourse 6.7.5 而非最新版Concourse 的版本迭代非常激进7.x 系列全面转向基于 containerd 的 worker 架构并废弃了对runc的直接支持。而 Ubuntu 16.04 的runc版本是 1.0.0-rc10它与 Concourse 7.x 的 API 协议不兼容。我实测过 Concourse 7.2.0 在 Ubuntu 16.04 上启动 web 节点后worker 会反复报错failed to resolve loader: thread-loader根源是其内部使用的go-grpc库调用了memfd_create()系统调用而该调用在 Linux 4.4 内核中默认未启用需CONFIG_MEMFD_CREATEy且 Ubuntu 16.04 内核配置里此项为m即模块化但未自动加载。Concourse 6.7.5 是最后一个稳定支持runc1.0.0-rc10 的大版本其 worker 使用的是garden-runc这是一个专为旧内核优化的轻量级容器运行时封装层它会自动 fallback 到chrootpivot_root的降级模式确保即使在最简陋的内核配置下也能创建隔离环境。更重要的是6.7.5 的flyCLI 工具与concourse-web的 API 兼容性极佳所有fly set-pipeline、fly trigger-job命令都能 100% 正常工作不会出现pip install requirement那种因 Python 包版本错配导致的 CLI 功能缺失。选择 6.7.5不是向后看而是向前看——它是在旧平台上实现 CI 流水线功能完整性的最优解。2.3 为什么坚持使用 systemd 而非 supervisord 或 screen网上很多 Ubuntu 16.04 的 Concourse 教程会教你用screen -S concourse-web ./concourse web ...启动这在调试阶段没问题但一旦投入生产就是灾难。screen进程没有健康检查、没有自动重启、没有资源限制、没有日志轮转更无法与系统启动流程深度集成。当服务器意外断电重启后screen会丢失所有会话Concourse 服务就永远停在那里直到有人 SSH 进去手动screen -r。而supervisord虽然比screen强但它本身是一个 Python 进程又引入了新的依赖链sudo apt-get install python-pip失败怎么办pip install supervisor时遇到failed to install homebrew portable ruby这类跨生态依赖错误怎么处理我们选择 systemd是因为它是 Ubuntu 16.04 的原生服务管理器无需额外安装且与内核深度绑定。通过编写.service文件我们可以精确控制Concourse web 必须在docker.socket就绪后才启动worker 必须等待 web 服务监听端口8080可达才开始注册如果进程崩溃systemd 会在 10 秒内自动拉起并记录完整的coredump供分析。这种级别的可靠性是任何第三方进程管理器都无法提供的。它不是为了炫技而是为了在无人值守的工厂车间里让 CI 服务像电灯开关一样按一下就亮关了再按还亮。3. 核心细节解析与实操要点3.1 环境准备绕过 apt 的“信任危机”Ubuntu 16.04 的apt仓库早已停止更新直接apt update apt upgrade会卡在http://archive.ubuntu.com/ubuntu/dists/xenial-updates/InRelease404 错误上。我们必须先切换镜像源。但注意不能简单地sed -i s/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g /etc/apt/sources.list因为清华源的 xenial 镜像已于 2023 年底下线。实测可用的是阿里云的old-releases镜像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执行后apt update会成功但你会发现apt list --upgradable显示一堆linux-image-*、linux-headers-*包可升级。绝对不要执行apt upgrade这些内核包升级会覆盖/boot/vmlinuz-4.4.0-xx-generic而 Concourse 的 Garden worker 严重依赖当前内核的cgroup控制组挂载点/sys/fs/cgroup下的子目录结构。一次内核升级后worker 启动时会报failed to install dependencies: failed to install d这里的d指的就是devicecgroup 子系统因为新内核的挂载方式变了。正确的做法是只升级最关键的ca-certificates和curl确保后续能安全下载 HTTPS 资源sudo apt install -y ca-certificates curl然后立刻禁用自动升级防止 cron-apt 在后台偷偷搞事情sudo systemctl stop apt-daily.service apt-daily.timer sudo systemctl disable apt-daily.timer提示这是我在三台不同品牌服务器上踩过的统一坑。某次凌晨自动升级后CI 流水线全部卡在waiting for worker状态查了 6 小时才发现是/sys/fs/cgroup/devices/目录权限被新内核重置为0755而 Garden 要求0700。手动chmod 0700 /sys/fs/cgroup/devices只能临时解决重启后又恢复。根源就是内核升级。3.2 Docker 配置为 Concourse worker “减负”Concourse worker 本身不运行 Docker daemon但它需要一个 Docker client 来与外部 registry 交互比如put一个 Docker image 到 Harbor。Ubuntu 16.04 自带的docker.io包足够用但默认配置有隐患。/etc/default/docker文件里有一行DOCKER_OPTS--storage-driveroverlay这在 Ubuntu 16.04 上是致命的因为 overlayfs 驱动在 4.4 内核上不稳定worker 创建任务容器时会随机报command [npm, install] returned这类看似无关的错误实际是 overlay 层写入失败导致的进程退出。解决方案是强制改用aufs驱动它在 4.4 内核上经过十年考验极其稳定echo DOCKER_OPTS--storage-driveraufs | sudo tee -a /etc/default/docker sudo modprobe aufs echo aufs | sudo tee -a /etc/modules sudo systemctl restart docker验证是否生效sudo docker info | grep Storage Driver应输出aufs。这一步做完Concourse worker 的baggageclaim组件才能稳定地为每个 build 创建干净的 rootfs 层避免playwright install chromium这类需要大量磁盘 I/O 的任务因文件系统抖动而超时失败。3.3 Concourse 二进制安装从官方 Release 页面“精准狙击”Concourse 官方 GitHub Release 页面https://github.com/concourse/concourse/releases提供了所有版本的预编译二进制。我们要找的是concourse-6.7.5-linux-amd64.tgz。注意不要下载concourse-6.7.5.tgz那是源码包。下载命令必须用curl -fL加-f参数否则遇到 CDN 缓存 404 会静默失败cd /tmp curl -fL https://github.com/concourse/concourse/releases/download/v6.7.5/concourse-6.7.5-linux-amd64.tgz | sudo tar -C /usr/local/bin -xzf - sudo chmod x /usr/local/bin/concourse验证安装concourse --version应输出6.7.5。这里有个关键细节/usr/local/bin目录必须在root用户的PATH环境变量里。Ubuntu 16.04 的 root PATH 默认不包含/usr/local/bin所以如果你用sudo concourse web启动会报command not found。解决方案是编辑/root/.profile在末尾添加export PATH/usr/local/bin:$PATH然后source /root/.profile。这解释了为什么很多教程里sudo concourse web能跑而你的不行——环境变量没对齐。4. 实操过程与核心环节实现4.1 初始化数据库PostgreSQL 9.5 的“温柔一刀”Concourse 依赖 PostgreSQL 存储 pipeline 定义、build 历史、worker 状态等元数据。Ubuntu 16.04 的apt仓库里只有 PostgreSQL 9.5这反而是好事因为 Concourse 6.7.5 对 PG 9.5 的兼容性经过了充分测试而 PG 12 的某些 WAL 日志格式变更会导致 Concourse 启动时failed to invoke tool webscraper。安装命令sudo apt install -y postgresql-9.5 postgresql-client-9.5 postgresql-contrib-9.5安装完成后PostgreSQL 服务会自动启动并监听localhost:5432。我们需要创建一个专用数据库和用户sudo -u postgres psql -c CREATE DATABASE concourse; sudo -u postgres psql -c CREATE USER concourse WITH PASSWORD changeme123; sudo -u postgres psql -c GRANT ALL PRIVILEGES ON DATABASE concourse TO concourse;关键点来了PostgreSQL 9.5 默认启用了password_encryption on它使用md5加密而 Concourse 6.7.5 的数据库连接池pq驱动要求明文密码或scram-sha-256。解决方案是修改/etc/postgresql/9.5/main/pg_hba.conf将local连接方式从md5改为trust仅限内网可信环境# TYPE DATABASE USER CIDR-ADDRESS METHOD local concourse concourse trust然后重启服务sudo systemctl restart postgresql。这步看似“不安全”但在 CI 服务器这种封闭内网环境中trust比md5更可靠它彻底消除了密码哈希算法不匹配导致的failed to resolve loader类连接错误。4.2 Web 节点配置TLS 证书的“零成本”方案Concourse web 节点必须启用 TLS否则flyCLI 无法连接。生成自签名证书最简单的方法是用openssl但 Ubuntu 16.04 的openssl版本较老不支持--addext参数。我们用经典三步法# 1. 生成私钥 openssl genrsa -out /etc/concourse/web.key 2048 # 2. 创建证书签名请求CSR关键是要把 DNS 名称写进 SAN cat /tmp/web.cnf EOF [req] distinguished_name req_distinguished_name x509_extensions v3_req prompt no [req_distinguished_name] C CN ST Beijing L Beijing O MyOrg OU CI CN concourse.example.com [v3_req] keyUsage keyEncipherment, dataEncipherment extendedKeyUsage serverAuth subjectAltName alt_names [alt_names] DNS.1 concourse.example.com DNS.2 localhost IP.1 127.0.0.1 IP.2 192.168.1.100 EOF # 3. 生成证书有效期设为 3650 天10年避免频繁更新 openssl req -new -x509 -days 3650 -key /etc/concourse/web.key -out /etc/concourse/web.crt -config /tmp/web.cnf其中IP.2必须填你服务器的真实内网 IP否则fly login -c https://192.168.1.100:8080会报x509: certificate is valid for concourse.example.com, localhost, 127.0.0.1, not 192.168.1.100。这解释了为什么很多人fly login失败却百思不得其解——证书里没写对 IP。4.3 Systemd 服务文件让 Concourse “活”在系统里创建 web 服务文件/etc/systemd/system/concourse-web.service[Unit] DescriptionConcourse CI Web Node Afternetwork.target docker.service postgresql.service Wantsdocker.service postgresql.service [Service] Typesimple Userroot Grouproot EnvironmentFile/etc/concourse/web.env ExecStart/usr/local/bin/concourse web Restarton-failure RestartSec10 LimitNOFILE65536 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target关键点是EnvironmentFile它指向一个独立的环境变量文件/etc/concourse/web.env内容如下CONCOURSE_EXTERNAL_URLhttps://concourse.example.com:8080 CONCOURSE_POSTGRES_DATA_SOURCEpostgres://concourse:changeme123127.0.0.1:5432/concourse?sslmodedisable CONCOURSE_ADD_LOCAL_USERtestuser:bcrypt-hash-of-password CONCOURSE_MAIN_TEAM_LOCAL_USERtestuser CONCOURSE_SESSION_SIGNING_KEY/etc/concourse/session_signing_key CONCOURSE_TSA_HOST_KEY/etc/concourse/tsa_host_key CONCOURSE_TSA_AUTHORIZED_KEYS/etc/concourse/authorized_worker_keys其中CONCOURSE_ADD_LOCAL_USER的密码 hash 必须用bcrypt生成。Ubuntu 16.04 没有htpasswd -B我们用 Python 一行搞定python -c import bcrypt; print(bcrypt.hashpw(btestpass, bcrypt.gensalt(rounds10)).decode()) /tmp/bcrypt然后把/tmp/bcrypt的内容粘贴到web.env里。CONCOURSE_SESSION_SIGNING_KEY等密钥文件用ssh-keygen -t rsa -b 4096 -f /etc/concourse/session_signing_key -N 生成。所有这些步骤都是为了让 Concourse web 在 systemd 的严格管控下带着正确的身份、密钥和数据库连接串稳稳地启动。4.4 Worker 节点注册打通“最后一公里”Worker 服务文件/etc/systemd/system/concourse-worker.service[Unit] DescriptionConcourse CI Worker Node Afternetwork.target concourse-web.service Wantsconcourse-web.service [Service] Typesimple Userroot Grouproot EnvironmentFile/etc/concourse/worker.env ExecStart/usr/local/bin/concourse worker Restarton-failure RestartSec10 LimitNOFILE65536 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target对应的/etc/concourse/worker.envCONCOURSE_TSA_HOST127.0.0.1:2222 CONCOURSE_TSA_PUBLIC_KEY/etc/concourse/tsa_host_key.pub CONCOURSE_TSA_WORKER_PRIVATE_KEY/etc/concourse/worker_key CONCOURSE_GARDEN_NETWORK_POOL10.254.0.0/16CONCOURSE_TSA_WORKER_PRIVATE_KEY用ssh-keygen -t rsa -b 4096 -f /etc/concourse/worker_key -N 生成。启动顺序至关重要必须先sudo systemctl start concourse-web等sudo journalctl -u concourse-web -n 50 | grep serving TSA on出现后再sudo systemctl start concourse-worker。否则 worker 会一直报failed to install dependencies: failed to install d因为它连不上 TSATeam Server Agent服务。我通常加一个简单的健康检查脚本while ! nc -z 127.0.0.1 2222; do sleep 1; done sudo systemctl start concourse-worker5. 常见问题与排查技巧实录5.1 “failed to install dependencies: failed to install d” 的终极诊断树这个错误信息极其误导人它几乎不指代真实的ddevice依赖而是 Garden worker 启动失败的通用占位符。我的排查清单如下检查项命令预期输出修复方法TSA 服务是否监听sudo ss -tlnp | grep :2222LISTEN 0 128 *:2222 *:* users:((concourse,pid1234,fd10))若无输出检查concourse-web是否启动journalctl -u concourse-web查serving TSA日志cgroup devices 子系统是否挂载mount | grep cgroup | grep devicescgroup on /sys/fs/cgroup/devices type cgroup (rw,relatime,devices)若无执行sudo mkdir -p /sys/fs/cgroup/devices sudo mount -t cgroup -o devices none /sys/fs/cgroup/devicesworker_key 权限是否正确ls -l /etc/concourse/worker_key*-r-------- 1 root root ... /etc/concourse/worker_keysudo chmod 600 /etc/concourse/worker_keyDocker socket 是否可访问sudo -u root ls -l /var/run/docker.socksrw-rw---- 1 root docker ... /var/run/docker.socksudo usermod -aG docker root然后sudo systemctl restart docker注意sudo usermod -aG docker root这一步极易被忽略。Concourse worker 进程以root用户运行但它需要读写 Docker socket而默认root不在docker用户组里。不加这行worker 会卡在waiting for baggageclaim最终超时抛出那个d错误。5.2 “command nvidia-smi not found” 的 CI 场景适配这个错误在 Concourse 中很常见当 pipeline 里有get一个包含 CUDA 工具链的 Docker image然后task里执行nvidia-smi时触发。根本原因不是没装 NVIDIA 驱动而是 Concourse worker 的 Garden 容器默认不挂载宿主机的/dev/nvidiactl、/dev/nvidia-uvm等设备节点。解决方案是在task.yml的platform下增加privileged: true但这在 Ubuntu 16.04 上有风险可能触发failed to launch plugin。更稳妥的做法是在 worker 启动时通过--garden-network-pool参数之外再加一个--garden-dns-server和--garden-dns-servers但这只是治标。治本之策是在宿主机上创建一个nvidia-devicessystemd 服务每次启动时自动挂载# /etc/systemd/system/nvidia-devices.service [Unit] DescriptionNVIDIA Device Nodes for Concourse Afterdocker.service [Service] Typeoneshot ExecStart/bin/sh -c for dev in /dev/nvidia*; do [ -e $dev ] mknod -m 666 /var/lib/concourse/dev/$(basename $dev) c $(stat -c %t %T $dev); done RemainAfterExityes [Install] WantedBymulti-user.target然后在concourse-worker.service的After行加上nvidia-devices.service。这样worker 启动时所有 NVIDIA 设备节点都已准备好nvidia-smi就能自然找到它们。5.3 “todo-tree: failed to find vscode-ripgrep” 的前端构建陷阱这个错误通常出现在 Concourse pipeline 的put一个前端应用到 Nginx 时task里执行npm run build报错。根源是 Concourse 的noderesource 默认拉取的node:16-alpine镜像里没有ripgrep而todo-tree插件依赖它。解决方案不是在 pipeline 里apt install ripgrepAlpine 用apk而是换一个更“胖”的基础镜像。在resources定义里把type: node改为type: docker-image并指定ubuntu:20.04镜像然后在task的config里显式安装- task: build-frontend config: platform: linux image_resource: type: docker-image source: repository: ubuntu tag: 20.04 inputs: - name: source-code outputs: - name: built-dist run: path: sh args: - -c - | apt-get update apt-get install -y nodejs npm ripgrep \ cd source-code npm ci npm run build \ cp -r dist/* ../built-dist/Ubuntu 20.04 的apt仓库里有ripgrep且nodejs版本与npm兼容性好彻底避开npm install 一直转圈的网络问题。5.4 “pip install pyinstaller” 与 Python 环境的“幽灵冲突”当 pipeline 里需要打包 Python 应用时pip install pyinstaller常失败报错could not install packages due to an oserror。这是因为 Concourse 的pythonresource 默认使用python:3.9-slim它基于 Debian而 Ubuntu 16.04 的glibc是 2.23Debian slim 镜像的glibc是 2.31存在 ABI 不兼容。解决方案是强制使用ubuntu:16.04作为基础镜像并在task里用apt安装 Python- task: build-python-app config: platform: linux image_resource: type: docker-image source: repository: ubuntu tag: 16.04 inputs: - name: source-code outputs: - name: built-binary run: path: sh args: - -c - | apt-get update apt-get install -y python3 python3-pip python3-setuptools \ pip3 install --upgrade pip setuptools \ pip3 install pyinstaller \ cd source-code pyinstaller --onefile myapp.py \ cp dist/myapp ../built-binary/这样Python 环境与宿主机 ABI 完全一致pyinstaller打包出的二进制能在 Ubuntu 16.04 上 100% 运行。6. 生产环境加固与长期维护心得6.1 日志轮转别让/var/log/journal吃光磁盘Concourse 的日志量巨大journalctl -u concourse-web默认保存所有历史几个月下来/var/log/journal能涨到 20G。Ubuntu 16.04 的systemd-journald默认配置不启用轮转。编辑/etc/systemd/journald.conf取消以下三行的注释SystemMaxUse500M SystemMaxFileSize100M MaxRetentionSec1month然后sudo systemctl restart systemd-journald。这能确保日志只保留最近一个月且单个文件不超过 100M总大小压在 500M 以内。我见过太多次 CI 服务器因日志塞满/var分区而整个宕机df -h第一时间就要看这里。6.2 数据库备份用pg_dump写个“保命脚本”Concourse 的 PostgreSQL 数据库是单点故障源。我写了一个每天凌晨 2 点执行的备份脚本/usr/local/bin/backup-concourse-db.sh#!/bin/bash DATE$(date %Y%m%d) PGPASSWORDchangeme123 pg_dump -h 127.0.0.1 -U concourse concourse | gzip /backup/concourse-$DATE.sql.gz find /backup -name concourse-*.sql.gz -mtime 30 -delete然后sudo crontab -e添加0 2 * * * /usr/local/bin/backup-concourse-db.sh。关键是PGPASSWORD环境变量它绕过了.pgpass文件的复杂配置简单粗暴有效。备份文件保留 30 天足够应对绝大多数误操作恢复场景。6.3 版本冻结给 Concourse “打上封条”Concourse 6.7.5 是我们选定的黄金版本绝不允许被意外升级。在/etc/apt/preferences.d/concourse-pin里写入Package: concourse* Pin: version 6.7.5* Pin-Priority: 1001然后sudo apt-mark hold concourse。这样即使未来某天apt upgrade命令被执行Concourse 也不会被波及。这招在多团队共用一台 CI 服务器时特别管用能防止某个实习生手滑apt upgrade毁掉整个流水线。我个人在实际操作中的体会是在 Ubuntu 16.04 上部署 Concourse不是一场技术秀而是一次对系统底层耐心的极限测试。每一个failed to install、command not found、connection refused的背后都不是软件的 bug而是旧世界与新理念之间那道需要亲手去弥合的缝隙。当你终于看到fly workers输出runningfly pipelines列出你定义的流水线那一刻的踏实感远胜于在云上一键部署十台 Kubernetes 集群。因为你知道这台老服务器上的每一行日志、每一个进程、每一块磁盘都在你的掌控之中。它不酷但它可靠它不新但它能干活。这才是工程的本质。
返回列表