前言
腾讯云健康看板(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之上就是运营商的域名解析服务了,这里按照运营商的要求配置就行。
由此得到前端服务的整体部署:
在这种部署模式下出现了两个问题:
- Node服务的访问日志记录不够完整,信息不够全
- 静态资源的访问也会经过Node服务的转发,对Node服务压力比较大,压测性能不佳
问题1:访问日志记录困难
虽然在网关层可以记录用户的访问日志,但是我们仍然希望在服务层进行日志记录。不仅仅是记录访问日志,还有异常日志。考虑到Next.js框架的全栈能力,自然而然地决定用 log4js 这样的日志库进行Node服务日志的收集。
下面的代码定义了一个utils
,对外抛出一个logger
对象,需要记录日志时只需要调用logger.info()
、logger.error()
方法即可。因为同时配置了 stdout
和dateFile
类型的 appenders
,日志会打印在控制台并持久化存储到 /logs
目录下
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
有点力不从心,主要的问题有:
- 无法自动记录
http/https
请求日志; - 日志记录对代码有一定侵入性;
- 无法记录请求耗时。
腾讯云status page应用因为需要支持SSG,Next.js的一些特性无法使用,比如:getServerSideProps
,这就意味着难以记录到 req
和 res
对象里的信息。
其实在Next.js 的SSG模式下也提供一种途径获取请求的
req
和res
,那就是 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
并发级别20
ab -n 100 -c 20 http://localhost:3000/_next/static/css/b2d4c37da20f311d.css
并发级别50
ab -n 100 -c 50 http://localhost:3000/_next/static/css/b2d4c37da20f311d.css
通过压测数据可以发现,当并发级别达到50时,响应耗时急剧增加,达到并发级别为20时的3倍以上耗时。
部署优化-增加Nginx代理层
为了解决直接暴露Node服务部署模式的问题,我们决定引入Nginx
中间件进行访问日志记录和静态资源代理。
构建自己的基础镜像
Q: 为什么要构建自己的基础镜像,公共仓库的镜像不香吗?
A:首先dockerhub镜像仓库没有同时支持Nginx Node.js运行时的基础镜像(有个人上传的镜像,但因为不透明,不太安全)。
其次,在Nginx基础镜像
或Node.js基础镜像
中通过 RUN
指令动态安装缺少的运行时环境有两个不足:
- 会增加流水线执行部署耗时且总是会因为公司防火墙的关系造成安装失败(安装Nginx or Node.js会超时)
- 增加部署镜像的体积(我们对比
Nginx基础镜像
vsNginx Node.js基础镜像
最终构建出的部署镜像体积,两者相差20MB)
构建基础镜像的步骤基本是按照官方命令来做,这里不在赘述。我们把镜像托管在公司自建的dockerhub上:
该基础镜像主要的特性有:
- 安装有Nginx,
Nginx version: Nginx/1.23.4
- 安装有Node.js,
v18.14.2
- 时区设置为
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
如下:
# 使用自定义镜像
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
的配置文件:
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;
}
修改点主要有:
- 设置
$loggable
条件,对于HEAD
,OPTIONS
请求不记录日志 - 设置日志格式,增加
request_time
、upstream_connect_time
、upstream_header_time
、upstream_response_time
时间数据,关于这四个时间的含义可以看下图
nginx.conf
nginx.conf
是用来替换 /etc/nginx/conf.d/default.conf
,主要是配置具体的代理策略。
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
ab -n 500 -c 100 http://localhost:80/_next/static/css/b2d4c37da20f311d.css
在并发级别为50的条件下,使用Nginx代理后性能远超直接访问Node服务(性能提升近20倍)。
总结
Next.js应用需要Node.js运行时,也就限定了其不能使用COS静态资源部署模式。docker容器化部署也存在直接暴露Node服务和通过Nginx代理Node服务后再进行暴露两种方式。这两种方式没有绝对的好与不好,只有适用与不适用。
在实际访问中,我们发现了直接暴露Node服务存在的问题:
- 日志记录不全
- 压测性能不佳
所以才有了增加Nginx层转发的思路,进一步的,借助Nginx的代理能力,不经过Node服务,直接由Nginx代理所有静态资源的访问,可以进一步提高访问性能。
快速访问
- 腾讯云Status Page
- 国际站Status Page
系列文章
- 腾讯云Status Page(健康看板)简介
- 腾讯云Status Page(健康看板)前端部署实践
- 腾讯云Status Page(健康看板)服务端渲染实践
- 腾讯云Status Page(健康看板)容灾设计与混沌演练实践——上篇
- 腾讯云Status Page(健康看板)容灾设计与混沌演练实践——下篇
参考
- Nextjs
- Nginx