Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Blog] 全栈项目全流程上线、部署、运维实践 #65

Open
lvqq opened this issue Dec 21, 2024 · 0 comments
Open

[Blog] 全栈项目全流程上线、部署、运维实践 #65

lvqq opened this issue Dec 21, 2024 · 0 comments
Labels

Comments

@lvqq
Copy link
Owner

lvqq commented Dec 21, 2024

本文主要记录了笔者个人全栈项目上线、部署、运维的整个流程实践,仅供参考。话不多说,先上一个效果图:

image

基本信息

服务器购自阿里云 ECS,操作系统为 Debain 10,项目的技术栈采用的是前端 React,后端 NestJS,数据库 MySQL,搭建的监控告警系统则是选用的 PrometheusGrafana 的系列工具

环境准备

如果你的服务器中已准备好了相关的镜像或者环境,可以跳过这部分

Git

安装 git:

sudo apt update
sudo apt install git

生成 ssh key 后配置到 Github 中:

ssh-keygen -t ed25519 -C "youremail@email.com"

MySQL

安装

由于 Debain 的 apt 包中没有 mysql-server,仅支持 mariadb-server,这里选择手动安装的方式。下载 MySQL 5.7,或者前往官网寻找合适的版本:

curl -OL https://downloads.mysql.com/archives/get/p/23/file/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz

然后解压缩:

sudo tar -xzf mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz -C /usr/local

重命名目录:

cd /user/local
sudo mv mysql-5.7.44-linux-glibc2.12-x86_64 mysql

创建 MySQL 用户和组:

sudo groupadd mysql
sudo useradd -r -g mysql -s /bin/false mysql

初始化数据库:

cd /usr/local/mysql
sudo mkdir mysql-files
sudo chmod 750 mysql-files
sudo chown -R mysql:mysql ./
sudo bin/mysqld --initialize --user=mysql

此时终端中会显示随机生成的密码:

[Note] A temporary password is generated for root@localhost: %sqwtmz5p(Xe

设置权限和目录:

sudo chown -R root .
sudo chown -R mysql data mysql-files

安装启动脚本:

sudo cp support-files/mysql.server /etc/init.d/mysql
sudo chmod +x /etc/init.d/mysql
sudo update-rc.d mysql defaults

启动 MySQL 服务:

sudo systemctl start mysql

查询 MySQL 服务状态:

sudo systemctl status mysql

修改密码:

# 登录 root
mysql -u root -p

# 执行 SQL
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password';

远程连接

创建一个新的用户用于远程连接:

# % 表示任意地址,也可以指定具体的 ip
CREATE USER 'remote'@'%' IDENTIFIED BY 'newpassword'

授予权限:

# 所有数据库:
GRANT ALL PRIVILEGES ON *.* TO 'newuser'@'%';

# 特定数据库
GRANT ALL PRIVILEGES ON exampledb.* TO 'newuser'@'%';

刷新权限:

FLUSH PRIVILEGES;

配置 MySQL 允许远程连接,编辑或新建 /etc/mysql/my.cnf/etc/my.cnf

[mysqld]
bind-address = 0.0.0.0

然后重启 MySQL 服务:

sudo systemctl restart mysql

NodeJS

安装 nvm 来管理 NodeJS 版本,官方的安装方式对于网络连通性有要求,采用下面的方法:

git clone https://github.com/nvm-sh/nvm.git
bash nvm/install.sh

安装 NodeJS 20:

nvm install 20

设置 npm 镜像:

echo 'registry=https://registry.npmmirror.com/' > ~/.npmrc

Nginx

安装 Nginx:

sudo apt install nginx

服务部署

NodeJS 服务

使用 pm2 来启动 NodeJS 服务:

pm2 start ecosystem.config.js --env production

配置文件示例:

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'my-app',
      script: 'dist/main.js',
      instances: 'max',
      exec_mode: 'cluster',
      env: {
        NODE_ENV: 'development'
      },
      env_production: {
        NODE_ENV: 'production'
      }
    }
  ]
};

pm2 默认使用用户级别的配置和进程管理,如果期望多用户共享 PM2 的状态和实例,可以设置 PM2_HOME 环境变量以实现共享:

export PM2_HOME=/home/your_user/.pm2

Nginx

配置 Nginx 转发服务端接口:

cd /etc/nginx/sites-available/
vi api.myhost.com

配置文件中添加:

server {
    listen 80;
    server_name api.myhost.com;

    location / {
        proxy_pass http://localhost:3000;
        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;
    }
}

新建软连接以启动对应配置:

sudo ln -s /etc/nginx/sites-available/api.myhost.com /etc/nginx/sites-enabled/

重启 Nginx 应用配置:

sudo systemctl restart nginx

配置 Nginx 转发前端静态资源,流程和前面一致,仅 Nginx 配置略有不同:

server {
    listen 80;
    server_name www.example.com;

    root /root/projects/example/dist;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }
}

HTTPS 配置

通过 certbot 申请 SSL 证书,安装对应 pkg:

sudo apt install certbot python3-certbot-nginx

根据提示完成配置,将自动下载证书,完成 nginx 配置并重启:

sudo certbot --nginx -d api.myhost.com

证书有效期只有 3 个月,可以通过脚本实现自动续签,参考 certbot-dns-aliyun

# 安装 certbot-dns-aliyun
wget https://cdn.jsdelivr.net/gh/justjavac/certbot-dns-aliyun@main/alidns.sh
sudo cp alidns.sh /usr/local/bin
sudo chmod +x /usr/local/bin/alidns.sh
sudo ln -s /usr/local/bin/alidns.sh /usr/local/bin/alidns
rm alidns.sh

测试证书续期:

certbot renew --manual --preferred-challenges dns --manual-auth-hook "alidns" --manual-cleanup-hook "alidns clean" --dry-run

正式续期时去掉 --dry-run

certbot renew --manual --preferred-challenges dns --manual-auth-hook "alidns" --manual-cleanup-hook "alidns clean"

设置定时任务:

crontab -e

设置每天凌晨 1 点 1 分执行:

1 1 */1 * * certbot renew --manual --preferred-challenges dns --manual-auth-hook "alidns" --manual-cleanup-hook "alidns clean" --deploy-hook "nginx -s reload"

查询 crontab 执行记录:

grep CRON /var/log/syslog

CD

Github Workflow 为例,实现自动化部署。新建 .github/workflows/deploy.yml

name: Deploy to Server

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up SSH
      uses: webfactory/ssh-agent@v0.5.3
      with:
        ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

    - name: Run deployment script
      run: |
        ssh -o StrictHostKeyChecking=no user@your-server-ip << 'EOF'
        cd /path/to/your/repo
        git pull origin main
        pnpm install
        pnpm run build
        pm2 restart your-app-name
        EOF

新建一个用户用于托管 CD 流程:

sudo adduser deploy

给用户设置某个目录的所有权:

sudo chown -R deploy:deploy /var/www/html

切换至对应用户:

sudo su - deploy

在 Github Repo -> Setting -> Sercets and variables -> Actions 中配置你的 SSH 私钥即可实现自动化部署。部分命令需要手动 export 才可以访问到:

export PATH="$PATH:/home/deploy/.nvm/versions/node/v20.18.1/bin"

服务运维

监控

数据采集

NestJS 中通过 Prometheus 中间件来收集服务级别的监控指标数据,一个收集 request 请求数的例子:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Counter, register } from 'prom-client';
import { NextFunction, Request, Response } from 'express';

@Injectable()
export class MetricsMiddleware implements NestMiddleware {
  private readonly requestCounter: Counter<string>;

  constructor() {
    this.requestCounter = new Counter({
      name: 'http_requests_total',
      help: 'Total number of requests',
      labelNames: ['method', 'route', 'status_code'],
    });
    register.registerMetric(this.requestCounter);
  }

  use(req: Request, res: Response, next: NextFunction): void {
    res.on('finish', () => {
      const labels = {
        method: req.method,
        route: req.path,
        status_code: res.statusCode.toString(),
      };

      this.requestCounter.inc(labels);
    });

    next();
  }
}

通过 pm2-metrics 来收集 pm2 的进程级别的监控指标数据,默认会在 9209 端口启动:

pm2 install pm2-metrics

通过 mysqld-exporter 来收集 MySQL 的数据库级别的监控指标数据:

wget https://github.com/prometheus/mysqld_exporter/releases/download/v0.16.0/mysqld_exporter-0.16.0.linux-amd64.tar.gz
tar xzvf mysqld_exporter-0.16.0.linux-amd64.tar.gz
cd mysqld_exporter-0.16.0.linux-amd64.tar.gz
sudo mv mysqld_exporter /usr/local/bin/

首先登录 MySQL:

mysql -u root -p

然后创建一个新的 MySQL 用户用于收集指标数据:

CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'your_password' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';

通过 systemctl 的方式来管理 mysqld_exporter 服务,创建 mysqld_exporter 用户和相关文件:

sudo useradd --no-create-home --shell /bin/false mysqld_exporter
sudo chown mysqld_exporter:mysqld_exporter /usr/local/bin/mysqld_exporter
sudo mkdir /etc/mysqld_exporter
sudo chown mysqld_exporter:mysqld_exporter /etc/mysqld_exporter

创建配置文件 /etc/mysqld_exporter/.my.cnf

[client]
user=exporter
password=your_password
host=localhost

设置文件权限:

sudo chown mysqld_exporter:mysqld_exporter /etc/mysqld_exporter/.my.cnf
sudo chmod 600 /etc/mysqld_exporter/.my.cnf

创建 systemd 服务文件,新建 /etc/systemd/system/mysqld_exporter.service 文件:

[Unit]
Description=mysql_exporter
Wants=network-online.target
After=network-online.target

[Service]
User=mysqld_exporter
Group=mysqld_exporter
Type=simple
ExecStart=/usr/local/bin/mysqld_exporter \
  --config.my-cnf /etc/mysqld_exporter/.my.cnf

[Install]
WantedBy=multi-user.target

启动服务:

sudo systemctl start mysqld_exporter

数据拉取 & 存储

利用 Prometheus 来收集、处理指标数据。从官网下载对应操作系统的版本:

wget https://github.com/prometheus/prometheus/releases/download/v3.0.1/prometheus-3.0.1.linux-amd64.tar.gz

解压缩:

tar xvfz prometheus-3.0.1.linux-amd64.tar.gz

cd prometheus-3.0.1.linux-amd64

编辑 prometheus.yml 配置文件:

scrape_configs:
  - job_name: 'mysql'
    static_configs:
      - targets: ['localhost:9104']

  - job_name: 'pm2'
    static_configs:
      - targets: ['localhost:9209']

  - job_name: 'app'
    static_configs:
      - targets: ['localhost:4000']

启动 prometheus 服务:

./prometheus --config.file=prometheus.yml

如果服务有如下报错:

Failed to determine correct type of scrape target." component="scrape manager" scrape_pool=pm2 target="http://localhost:9209/metrics?fallback_scrape_protocol=Prometheus" content_type="" fallback_media_type="" err="non-compliant scrape target sending blank Content-Type and no fallback_scrape_protocol specified for target

参考 issue 来给 exporter.js 添加 header,然后重启:

pm2 restart pm2-metrics

http://{your_ip}:9090 端口的可视化页面 Status -> Target Health 中看到 pm2 的 State 为 UP 则表示配置成功,可以在 Query -> Graph 中查询进行验证:

pm2_cpu

通过 systemctl 来管理 prometheus 服务,先将二进制文件移动至目录中:

sudo mv prometheus /usr/local/bin/
sudo mv promtool /usr/local/bin/

创建数据目录和配置目录:

sudo mkdir /etc/prometheus
sudo mkdir /var/lib/prometheus
sudo cp prometheus.yml /etc/prometheus/prometheus.yml

创建 prometheus 用户:

sudo useradd --no-create-home --shell /bin/false prometheus
sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus
sudo chown prometheus:prometheus /usr/local/bin/prometheus
sudo chown prometheus:prometheus /usr/local/bin/promtool

创建 systemd 服务文件,新建 /etc/systemd/system/prometheus.service 文件:

[Unit]
Description=Prometheus
Wants=network-online.target
After=network-online.target

[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/prometheus \
  --config.file=/etc/prometheus/prometheus.yml \
  --storage.tsdb.path=/var/lib/prometheus/

[Install]
WantedBy=multi-user.target

启动服务:

sudo systemctl start prometheus

数据可视化

通过 Grafana 消费 Prometheus 的监控数据,搭建可视化监控看板。安装 Grafana

sudo apt-get install -y apt-transport-https software-properties-common
sudo mkdir -p /etc/apt/keyrings/
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
sudo apt-get install grafana

服务默认端口是 3000,如果想要修改,需要编辑配置文件:

sudo vi /etc/grafana/grafana.ini

启动服务:

sudo systemctl start grafana-server

访问 Grafana 站点进行 Dashboard 配置,在 DataSource 中导入 Prometheus 数据源即可开始使用。

日志

日志轮转

pm2 默认日志单文件存储,通过 pm2-logrotate 来轮转 pm2 中的 NestJS 日志,减少磁盘消耗

# 安装 pm2-logrotate
pm2 install pm2-logrotate

使日志按每小时划分:

# 设置日志文件名
pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-00-00
pm2 set pm2-logrotate:rotateInterval "0 * * * *"

使用自定义的 logger 来实现 ORM 日志按小时划分:

class CustomLogger implements Logger {
  private getLogFileName(): string {
    const now = dayjs().set('minute', 0).set('second', 0);
    return path.join(LOG_DIR, `ormlogs-${now.format('YYYY-MM-DD_HH-mm-ss')}.log`);
  }

  // ...
}

日志收集 & 分析

通过 Loki 来收集、聚合日志:

sudo apt-get install loki promtail

编辑配置文件 /etc/promtail/config.yml:

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
- url: http://localhost:3100/loki/api/v1/push

scrape_configs:
- job_name: myjob
  static_configs:
  - targets:
      - localhost
    labels:
      job: myapp
      __path__: /my/log/path/*.log

重启服务:

sudo systemctl restart promtail

检查服务状态:

sudo systemctl status promtail
sudo systemctl status loki

可视化查询

GrafanaDataSource 中导入 Loki 数据源,即可在 Logs 中进行可视化查询。一个示例:

image-1

告警

Grafana 看板中可以方便地配置告警规则,并支持多种通知方式:

  • 邮件
  • Webhook
  • DingDing
  • Discord
  • Slark 等

这里以配置网易邮箱的 SMTP 服务进行邮件转发为例,首先访问邮箱在线地址,进入邮箱设置,开启 SMTP 并获得授权码,然后编辑服务器中的 /etc/grafana/grafana.ini

[smtp]
enabled = true
host = smtp.163.com:587
user = your_163_email@163.com
password = your_authorization_code
skip_verify = false
from_address = your_163_email@163.com
from_name = Grafana
ehlo_identity =

然后重启 Grafana 服务即可:

sudo systemctl restart grafana-server

测试邮件能否收到:

image-2

总结

随着环境和服务配置的完成,我们还通过引入的持续交付(CD)流程来实现了自动化部署,提高了开发和运维的效率。进一步地,我们通过监控、日志和告警的有效管理,能够实时掌握服务的运行状况并及时处理潜在问题。至此,已经搭建了一套相对完整的服务器环境和服务运维体系。

@lvqq lvqq added the blog label Dec 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant