标签: Nuxt

  • 一个 MySQL 实例喂五个应用:库隔离与两个连接池

    这台机器上只有一个 MySQL 实例,却被 WordPress、二维码平台、在线工具箱同时用着。下面的取舍和踩坑记录,对任何”一个数据库服务多个应用”的场景都适用。

    按库隔离,不按表前缀

    每个应用一套独立的库,按环境再分:

    wordpress_prod   / wordpress_beta
    qrcode_prod      / qrcode_beta
    tools_prod       / tools_beta
    users_prod       / users_beta   ← 注意这个

    用独立库而不是”一个库 + 表名前缀”,好处是备份、迁移、授权都能按库粒度做,某天要把某个应用挪走也不用在一堆表里挑挑拣拣。

    为什么用户表要单独一个库

    这是我改动最大的一个设计。最初每个应用各自建用户表,结果就是:在二维码平台注册的用户,到工具箱还得再注册一次。

    后来把用户表抽出来放进独立的 users_* 库,两个应用连同一个用户表,配合同一把 JWT 密钥,登录态就打通了。

    代价是应用侧要维护两个连接池

    getDB()      → 连业务库(qrcode_* / tools_*)
    getAuthDB()  → 连共享用户库(users_*)

    多一个连接池不算复杂,但它换来的是”一次注册,全站通用”。对用户来说是天壤之别。

    坑一:mysql2 不接受 undefined

    这个坑的表现非常迷惑:接口返回 500,但代码看起来毫无问题。

    // 报错
    await db.query('INSERT INTO t (a, b) VALUES (?, ?)', [foo, bar]);
    // bar 是 undefined 时 → 500

    原因是 mysql2 的绑定参数只接受 null,传 undefined 会直接抛错。而 JavaScript 里”对象上不存在的属性”返回的就是 undefined,这在拼装请求体时太常见了。

    解决办法是在数据库访问层统一做一次转换,而不是在每个调用点小心翼翼:

    function toParams(arr) {
      return arr.map((v) => (v === undefined ? null : v));
    }

    在数据访问层收口,比靠人记得写 ?? null 可靠得多。

    坑二:连接的时区必须等于实例的时区

    这个坑更隐蔽——它不报错,只是读出来的时间整体偏移几个小时

    MySQL 连接如果不显式指定时区,驱动会按自己的默认值解释 TIMESTAMP。容器里默认是 UTC,而你可能下意识按东八区理解,于是所有时间都差 8 小时。

    // 线上实例是 UTC,就明确写 Z
    createPool({
      timezone: 'Z',
      // ...
    });

    判断实例实际时区:

    SELECT @@global.time_zone, @@session_time_zone, NOW();

    然后让连接池的 timezone 和它一致。不要靠猜。

    这个方案的真实风险

    必须说清楚:单实例 MySQL 就是单点。它挂了,所有应用一起挂。

    对个人项目来说这个风险可以接受,但前提是要做到两件事——定期备份,并且真的演练过一次恢复。没有演练过的备份等于没有备份。

  • Traefik 子路径路由:一个域名挂多个应用

    这个域名下挂着三套完全独立的东西:根路径是 WordPress,/qrcode 是一个 Nuxt 应用(二维码与文件分享),/tools 是另一个 Nuxt 应用(在线图片工具)。它们没有共用任何代码,只是恰好共用一个域名。

    让这件事成立的是 Traefik 的 Ingress 路径匹配。

    路径规则

    Ingress 里按前缀分流,一个 host 下三条规则:

    rules:
      - host: www.mazter.cn
        http:
          paths:
            - path: /qrcode
              pathType: Prefix
              backend:
                service:
                  name: svc-qrcode-app
                  port:
                    number: 80
            - path: /tools
              pathType: Prefix
              backend:
                service:
                  name: svc-tools-app
                  port:
                    number: 80
            - path: /
              pathType: Prefix
              backend:
                service:
                  name: svc-wordpress
                  port:
                    number: 80

    匹配规则是最长前缀优先/tools/compress 先匹配 /tools 而不是 /。所以顺序写在前面后面其实不影响结果,但把具体的写在前面更符合直觉。

    要不要 stripPrefix

    这是最容易踩的一个决策点。所谓 stripPrefix,是指 Ingress 把 /qrcode 这段前缀剥掉再转给后端,后端收到的就是 /

    我没有用它,理由是:现代前端框架(Nuxt、Next、Vite)都要求在构建期就知道自己的 base 路径。如果让 Ingress 偷偷把前缀剥掉,前端生成的资源链接和实际访问路径就会对不上,出现”页面能打开但 JS/CSS 全 404″。

    做法是让应用自己知道前缀,构建时注入:

    # Nuxt 构建期指定 baseURL
    nuxt.config.ts 中:
      app: {
        baseURL: '/qrcode/'
      }

    于是后端收到的请求路径仍然是 /qrcode/xxx,应用自己也按 /qrcode 生成链接,两边对得上。

    Middleware 的 API 组陷阱

    Traefik v2 和 v3 的 CRD 分属不同的 API 组,写错的话资源能创建但完全不生效,而且不报错:

    # v2(当前集群是 v2.11.18)
    apiVersion: traefik.containo.us/v1alpha1
    
    # v3
    apiVersion: traefik.io/v1alpha1

    判断方法很简单,看集群里实际装了哪些 CRD:

    kubectl get crd | grep traefik

    哪个组存在就用哪个。这个坑的表现是”我明明配了跳转 HTTPS,为什么不生效”,而 YAML 看起来毫无问题。

    证书:一个 host 只有一张

    三个应用共用一个 host,所以也只能有一张 TLS 证书。它们复用同一个 Secret(这里是 wp-tls),不需要为每个应用单独签发。cert-manager 按 host 签发,不是按路径。

    这意味着新增子路径应用时,证书部分什么都不用做——这是共用域名的另一个便利。

    什么时候该换子域名

    子路径方案的前提是所有应用”彼此信任、共用一套 Cookie 域”。我的两个应用确实需要共享登录态(同一个 mazter_token Cookie 挂在根路径上),所以子路径是加分项。

    如果某个应用需要完全独立的 Cookie、独立的安全策略,或者要交给别人维护,那就该拆成子域名。判断标准不是技术难度,是信任边界