【云顾问-健康看板】腾讯云Status Page(健康看板)前端部署实践

2024-03-11 19:19:57 浏览数 (2)

前言

腾讯云健康看板(Tencent Cloud Health Dashborad,下面简称:腾讯云status page ),采用了Next.js全栈框架实现SSR SSG。

Next.js应用的部署需要Node.js 运行时,这就限定了无法采用腾讯云对象存储服务COS实现轻量化部署,需要借助腾讯云TKE进行容器化部署。在容器化部署方式上,腾讯云status page前期是直接暴露Node服务给域名网关,在域名网关层做负载均衡以期减少前端服务的部署层次。在应用持续演进过程中遇到了难以在服务层记录访问日志、压力测试表现不佳的问题。

综合考量后部署方案演进为Nginx Node服务的部署模式,借助Nginx优秀的性能,将访问层日志收集功能直接放到Nginx层,并且使用Nginx直接代理静态资源,减轻Node服务的压力。

初始方案(直接暴露Node服务)

这也是社区通行的部署模式,依托公司对应用上云的支持,团队可以方便地在TKE上申请Workload资源,每个Workload下可以部署多个Pod,Pod与Node服务按照1:1的比例进行配置,Workload作为前端服务的的实际承载。

为了容灾,前端部署采用的异地多活模式,在广州、上海、新加坡分别部署有Workload。通过接入CLB实现了异地多活服务的流量分配,在CLB之上就是运营商的域名解析服务了,这里按照运营商的要求配置就行。

由此得到前端服务的整体部署:

在这种部署模式下出现了两个问题:

  1. Node服务的访问日志记录不够完整,信息不够全
  2. 静态资源的访问也会经过Node服务的转发,对Node服务压力比较大,压测性能不佳

问题1:访问日志记录困难

虽然在网关层可以记录用户的访问日志,但是我们仍然希望在服务层进行日志记录。不仅仅是记录访问日志,还有异常日志。考虑到Next.js框架的全栈能力,自然而然地决定用 log4js 这样的日志库进行Node服务日志的收集。

下面的代码定义了一个utils,对外抛出一个logger对象,需要记录日志时只需要调用logger.info()logger.error()方法即可。因为同时配置了 stdoutdateFile类型的 appenders,日志会打印在控制台并持久化存储到 /logs 目录下

代码语言:javascript复制
import * as log4js from 'log4js';

const LEVELS = {
  trace: log4js.levels.TRACE,
  debug: log4js.levels.DEBUG,
  info: log4js.levels.INFO,
  warn: log4js.levels.WARN,
  error: log4js.levels.ERROR,
  fatal: log4js.levels.FATAL,
};

const LAYOUT_CONSOLE = {
  type: 'pattern',
  pattern: '%[[%p] %d{yyyy/MM/dd-hh.mm.ss}%] at %x{callStack} %n %m',
  tokens: {
    callStack: (event: any) => `${event.fileName} ${event.lineNumber}:${event.columnNumber}`,
  },
};

log4js.configure({
  appenders: {
    console: {
      type: 'stdout',
      layout: LAYOUT_CONSOLE, // 日志内容格式
    },
    info: {
      type: 'dateFile',
      filename: 'logs/INFO.log',
      encoding: 'utf-8',
      // 配置 layout,此处使用自定义模式 pattern
      layout: {
        type: 'pattern',
        pattern: '{"date":"%d{yyyy-MM-dd hh:mm:ss:SSS}%","level":"%p","category":"%c","host":"%h","pid":"%z", "data":'%m'}',
      },
      pattern: '-yyyy-MM-dd',
      // 回滚旧的日志文件时,保证以 .log 结尾 (只有在 alwaysIncludePattern 为 false 生效)
      keepFileExt: true,
      // 输出的日志文件名是都始终包含 pattern 日期结尾
      alwaysIncludePattern: true,
    },
    debug: {
      type: 'dateFile',
      filename: 'logs/DEBUG.log',
      encoding: 'utf-8',
      layout: {
        type: 'pattern',
        pattern: '{"date":"%d{yyyy-MM-dd hh:mm:ss:SSS}%","level":"%p","category":"%c","host":"%h","pid":"%z", "data":'%m'}',
      },
      pattern: '-yyyy-MM-dd',
      keepFileExt: true,
      alwaysIncludePattern: true,
    },
  },
  categories: {
    // 设置默认的 categories
    default: { appenders: ['console', 'info'], level: 'info', enableCallStack: true },
  },
});

const logger = log4js.getLogger('default');
// 定义日志级别
logger.level = LEVELS.info;
export { logger };
export default logger;

日志记录效果:

  • 控制台直接输出
控制台日志控制台日志
  • 记录到日志文件
日志文件列表日志文件列表

虽然通过 log4js 实现了Node服务的运行日志收集,但对于用户访问日志,log4js 有点力不从心,主要的问题有:

  1. 无法自动记录http/https请求日志;
  2. 日志记录对代码有一定侵入性;
  3. 无法记录请求耗时。

腾讯云status page应用因为需要支持SSG,Next.js的一些特性无法使用,比如:getServerSideProps,这就意味着难以记录到 reqres 对象里的信息。

其实在Next.js 的SSG模式下也提供一种途径获取请求的reqres,那就是 middleware,但遗憾的是,middleware运行环境部署标准的Node.js runtime,而是Next.js自己内置的 Edge Runtime。很多Node.js的API在 Edge Runtime下都不支持。 简单来讲就是像 log4js 这样的日志库在middleware里无法正常运行。所以想在middleware里记录访问日志的路走不通。

问题2:压测性能不佳

这一点很好理解,Node.js因为线程模型的限制,高并发性能不佳,处理高并发请求会出现响应耗时过长的问题。下面是使用 ab 工具的测试结果(以请求某个css资源为例):

并发级别10

ab -n 100 -c 10 http://localhost:3000/_next/static/css/b2d4c37da20f311d.css

并发级别10并发级别10

并发级别20

ab -n 100 -c 20 http://localhost:3000/_next/static/css/b2d4c37da20f311d.css

并发级别20并发级别20

并发级别50

ab -n 100 -c 50 http://localhost:3000/_next/static/css/b2d4c37da20f311d.css

并发级别50并发级别50

通过压测数据可以发现,当并发级别达到50时,响应耗时急剧增加,达到并发级别为20时的3倍以上耗时。

部署优化-增加Nginx代理层

为了解决直接暴露Node服务部署模式的问题,我们决定引入Nginx中间件进行访问日志记录和静态资源代理。

增加Nginx代理层增加Nginx代理层

构建自己的基础镜像

Q: 为什么要构建自己的基础镜像,公共仓库的镜像不香吗?

A:首先dockerhub镜像仓库没有同时支持Nginx Node.js运行时的基础镜像(有个人上传的镜像,但因为不透明,不太安全)。

其次,在Nginx基础镜像Node.js基础镜像中通过 RUN 指令动态安装缺少的运行时环境有两个不足:

  1. 会增加流水线执行部署耗时且总是会因为公司防火墙的关系造成安装失败(安装Nginx or Node.js会超时)
  2. 增加部署镜像的体积(我们对比 Nginx基础镜像 vs Nginx Node.js基础镜像最终构建出的部署镜像体积,两者相差20MB

构建基础镜像的步骤基本是按照官方命令来做,这里不在赘述。我们把镜像托管在公司自建的dockerhub上:

该基础镜像主要的特性有:

  1. 安装有Nginx,Nginx version: Nginx/1.23.4
  2. 安装有Node.js,v18.14.2
  3. 时区设置为 Asia/Shanghai(北京时间)

更新dockerfile

旧的dockerfile:

代码语言:bash复制
# Install dependencies only when needed
FROM Node:alpine AS deps

WORKDIR /app

COPY . .
EXPOSE 80
ENV PORT 80
ENV NODE_TLS_REJECT_UNAUTHORIZED=0

# set the time zone to Zone 8
ENV TZ Asia/Shanghai
RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime 
    && echo ${TZ} > /etc/timezone

CMD ["node_modules/.bin/next", "start"]

上面的dockerfile表明使用 node:alpine基础镜像,然后设置时区为 Asia/Shanghai

在新的部署模式下,因为新的基础镜像已经设置的时区,所以无需再重复设置时区为东八区。而在启动命令中则需要同时启动 Nginx 和 Next 服务,新的dockerfile如下:

代码语言:bash复制
# 使用自定义镜像
FROM xxx/tchd-frontend/tchd-frontend:latest AS deps

WORKDIR /app

COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY ./nginx_default.conf /etc/nginx/nginx.conf
RUN rm -rf ./nginx.conf ./nginx_default.conf
COPY . .

EXPOSE 80

ENV NODE_TLS_REJECT_UNAUTHORIZED=0

# start docker
CMD ["sh", "-c", "node_modules/.bin/next start & nginx -g 'daemon off;'"]

配置nginx.conf

这里分成两个Nginx配置文件,其一用来配置Nginx日志格式,并设置日志阀,对于不感兴趣的请求不记录日志;其二用来配置静态资源代理,主要是代理 /_next/static 路径和 / 路径下的静态资源请求:

nginx_default.conf

nginx_default.conf是在/etc/nginx/nginx.conf基础上进行修改后用来替换/etc/nginx/nginx.conf的配置文件:

代码语言:bash复制
user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  2048;
}

worker_rlimit_nofile 10000;

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    map $request_method $loggable {
        HEAD 0;
        OPTIONS 0;
        default 1; 
    }

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    '"$proxy_host" "$upstream_addr" '
                    'request_time=$request_time '
                    'upstream_connect_time=$upstream_connect_time '
                    'upstream_header_time=$upstream_header_time '
                    'upstream_response_time=$upstream_response_time ';

    access_log  /var/log/nginx/access.log  main if=$loggable;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    gzip  on;
    include /etc/nginx/conf.d/*.conf;
}

修改点主要有:

  1. 设置$loggable条件,对于 HEAD, OPTIONS请求不记录日志
  2. 设置日志格式,增加request_timeupstream_connect_timeupstream_header_timeupstream_response_time时间数据,关于这四个时间的含义可以看下图
Nginx连接时间模型Nginx连接时间模型

nginx.conf

nginx.conf是用来替换 /etc/nginx/conf.d/default.conf,主要是配置具体的代理策略。

代码语言:bash复制
server {
    listen 80;
    server_name status.cloud.tencent.com;
    root /app;
    index .next/server/pages/index.html; 
    autoindex off;

    ########## 静态资源代理配置开始    ##########
    # Nginx代理static目录,减小对Node服务的压力(采用下面的写法更精确)
    location ~* /_next/static/.*(js|css|png|jpg|jpeg|svg|gif|ico)$ {
        rewrite /_next/(.*) /.next/$1 break;
        try_files $uri $uri/;
        expires 7d;
        add_header Cache-Control "public";
        gzip on;
        gzip_types text/plain text/css image/svg xml image/png application/javascript text/xml application/xml application/xml rss text/javascript;
    }

    location ~* /assets/.*(png|jpg|jpeg|svg|gif)$ {
        root /app/public;
        expires 7d;
        add_header Cache-Control "public";
        gzip on;
        gzip_types text/plain text/css image/svg xml image/png application/javascript text/xml application/xml application/xml rss text/javascript;
    }

    location /favicon.ico {
        root /app/public;
        expires 7d;
        add_header Cache-Control "public";
        gzip on;
        gzip_types image/x-icon;
    }
    ########## 静态资源代理配置结束    ##########

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

上面配置的关键是对 /_next/static/路径下静态资源的代理。使其可以不通过Node服务,而直接由Nginx返回。

Q:可以不配置对静态资源的代理吗?

A:仅从可访问性角度,直接将所有请求转发到 http://localhost:3000就可以实现服务的正常访问,但是会出现性能瓶颈,其主要是静态资源请求挤占了Node服务的连接数,造成服务拥塞,进而出现静态资源响应慢的问题:

静态资源访问慢静态资源访问慢

从上图可以发现一些静态资源虽然体积很小,但却需要花超过1s的时间响应成功,这是完全不可接受的。

最终效果

增加Nginx层不会影响到CLB、域名解析上的配置,因此可以直接更新部署,部署完后后访问站点可以在 Workload的日志栏看到Nginx的访问日志:

并且静态资源经过代理后其访问性能也是得到了进一步提升:

静态资源访问加速效果静态资源访问加速效果

使用ab工具进行压测:

ab -n 500 -c 50 http://localhost:80/_next/static/css/b2d4c37da20f311d.css

并发50并发50

ab -n 500 -c 100 http://localhost:80/_next/static/css/b2d4c37da20f311d.css

并发100并发100

在并发级别为50的条件下,使用Nginx代理后性能远超直接访问Node服务(性能提升近20倍)。

总结

Next.js应用需要Node.js运行时,也就限定了其不能使用COS静态资源部署模式。docker容器化部署也存在直接暴露Node服务和通过Nginx代理Node服务后再进行暴露两种方式。这两种方式没有绝对的好与不好,只有适用与不适用。

在实际访问中,我们发现了直接暴露Node服务存在的问题:

  1. 日志记录不全
  2. 压测性能不佳

所以才有了增加Nginx层转发的思路,进一步的,借助Nginx的代理能力,不经过Node服务,直接由Nginx代理所有静态资源的访问,可以进一步提高访问性能。

快速访问

  • 腾讯云Status Page
  • 国际站Status Page

系列文章

  • 腾讯云Status Page(健康看板)简介
  • 腾讯云Status Page(健康看板)前端部署实践
  • 腾讯云Status Page(健康看板)服务端渲染实践
  • 腾讯云Status Page(健康看板)容灾设计与混沌演练实践——上篇
  • 腾讯云Status Page(健康看板)容灾设计与混沌演练实践——下篇

参考

  • Nextjs
  • Nginx

0 人点赞