知识点补充
什么是 k8s ?
Kubernetes 是一个用于 容器集群 的自动化部署、扩容以及运维的开源 平台。
k8s诞生的目的
k8s 孕育的初衷是培育出一个组件及工具的生态,帮助大家减轻在公有云及私有云上运行应用的负担,换言之,使得大型分布式应用的构建和运维变得更加简单(当然,越简单的表面意味着越复杂的内部细节)。
什么是 Websocket ?
WebSocket 建立在 TCP 协议之上,并且与 HTTP 协议有着良好的兼容性,最大的特点就是服务器可以主动向客户端推送消息,客户端也可以主动向服务器发送消息 。
开始搭建一个nodejs 服务端项目
框架选型
再结合自身项目,最终选用了 koa。原因有以下几点:
- koa 既适用小型项目,又适用大型项目,为以后项目扩展做好准备。
- koa 有着丰富的中间件,比 Express 更强大,比 Egg 更简易。
- Koa 提供了更优于 Express 的错误处理。
- midway 在之前就有关注,但到今年 1 月份才发布 1.0 版本,持续关注吧,业务项目就先不用它了。
小白都是先写个 app.js,没毛病,这里是我琢磨出的可用的初版,未做用户身份认证。
// app.js
import "babel-polyfill";
import Koa from "koa2";
import route from "koa-route";
import websockify from "koa-websocket";
import isEmpty from './public/isEmpty.js';
import logger from './public/logger.js';
import indexRouter from './routes/index';
import monitoringRouter from './routes/Monitoring';
const app = websockify(new Koa());
/**
* 注册路由
*/
app.use(indexRouter.routes(), indexRouter.allowedMethods())
app.use(monitoringRouter.routes(), monitoringRouter.allowedMethods())
/**
* websocket 部分
*/
let ctxs = {};
app.ws.use(
route.all("/:type/:from", (ctx, next) => {
/* 获取用户信息 */
let query = ctx.request.query;
if (isEmpty(query) || isEmpty(query.type) || isEmpty(query.from)) {
ctx.websocket.close(500, '非法请求');
logger.logInfo('', 'websocket close', '非法请求', JSON.stringify(query));
}
let type = query.type;
let from = query.from;
logger.logInfo(from, 'websocket open', 'type', type);
if (isEmpty(ctxs[type])) {
ctxs[type] = {};
}
ctxs[type][from] = ctx;
/* 接收到消息并转发 */
ctx.websocket.on("message", message => {
if (type == "from") {
if (!isEmpty(ctxs["to"]) && !isEmpty(ctxs["to"][from])) {
ctxs["to"][from].websocket.send(message);
logger.logInfo("给" + from, 'websocket message', '发送了:', message);
}
} else if (type == "to") {
if (!isEmpty(ctxs["from"]) && !isEmpty(ctxs["from"][from])) {
ctxs["from"][from].websocket.send(message);
logger.logInfo("给" + from, 'websocket message', '发送了:', message);
}
}
});
/* 连接关闭时的处理 */
ctx.websocket.on("close", message => {
/* 连接关闭时, 清理 上下文数组, 防止报错 */
let hasFind = false;
for (const key in ctxs) {
const item = ctxs[key];
for (const itemKey in item) {
const user = item[itemKey];
if (ctx == user) {
delete ctxs[key][itemKey];
logger.logInfo(key, 'websocket close', '-', itemKey);
hasFind = true;
break;
}
}
if (hasFind) {
break;
}
}
logger.logInfo('close', 'websocket close', 'end', message);
});
})
);
/**
* 监听
*/
app.listen(3000);
优化项目基础配置
使用 import 代替 require
nodejs 对 es6 还不是完全的支持,使用 imort 的方法也有很多,例如使用 mjs 后缀就可以,我这里还是用了 babel 去做了处理。
新增一个 start.js (并安装 babel-polyfill(放在dependencies
中)/babel-preset-env/babel-register(放在devDependencies
中)),内容如下:
// start.js
require('babel-register')({
presets: ['env']
})
module.exports = require('./app.js')
在app.js 中使用 import "babel-polyfill";
引入 babel-polyfill。
log 输出到文件
新增 log.js 文件,可以将 koa 运行中的项目日志输出到 access.log 中。再结合 tail -f access.log
cat access.log
快速查询日志。
// log.js
import { config } from '../config/config.js';
import fs from 'fs';
let options = {
flags: 'a', // append模式
encoding: 'utf8', // utf8编码
};
// 添加format方法
Date.prototype.format = function (format) {
if (!format) {
format = 'yyyy-MM-dd HH:mm:ss';
}
// 用0补齐指定位数
let padNum = function (value, digits) {
return Array(digits - value.toString().length + 1).join('0') + value;
};
// 指定格式字符
let cfg = {
yyyy: this.getFullYear(), // 年
MM: padNum(this.getMonth() + 1, 2), // 月
dd: padNum(this.getDate(), 2), // 日
HH: padNum(this.getHours(), 2), // 时
mm: padNum(this.getMinutes(), 2), // 分
ss: padNum(this.getSeconds(), 2), // 秒
fff: padNum(this.getMilliseconds(), 3), // 毫秒
};
return format.replace(/([a-z]|[A-Z])(\1)*/ig, function (m) {
return cfg[m];
});
}
let logTime = new Date().format('yyyy-MM-dd');
let stdout = fs.createWriteStream(config.logDir + 'access.' + logTime + '.pipe', options);
let stderr = fs.createWriteStream(config.logDir + 'error.' + logTime + '.log', options);
// 创建logger
let logger = new console.Console(stdout, stderr);
logger.logInfo = (user = '', func = '', title = '', msg = '') => {
let time = new Date().format('yyyy-MM-dd HH:mm:ss.fff');
let str = `[${user}] [${func}] [${title}] [${msg}]`;
logger.info(`[${time}] - INFO - ${str}`);
}
export default logger;
使用方法:
logger.logInfo('-','login','-',JSON.stringify(result));
添加 MySQL
在搭建 MySQL 时,要注意 MySQL 连接不释放的问题。
现象:www.cnblogs.com/hibiscus-be…
后面通过看 mysql/PoolConnection.js 源码解决,网上很多 MySQL 的 Demo 都会出连接不释放的问题。
// mysql.js
import mysql from 'mysql';
import { config } from '../config/config.js';
import logger from './logger';
class Mysql {
constructor() {
}
create(configName) {
return mysql.createPool({
host: config[configName].host,
user: config[configName].username,
password: config[configName].password,
database: config[configName].dbname
});
}
query(dbName, sql, values) {
return new Promise((resolve, reject) => {
let poolName = '';
switch (dbName) {
case 'db_name1': {
poolName = 'db1';
} break;
case 'db_name2': {
poolName = 'db2';
} break;
default: { resolve([]); } break;
}
if (poolName == '') {
resolve([]);
return;
}
let pool = this.create(poolName);
pool.getConnection((err, connection) => {
if (err) {
logger.logInfo('-', 'err', 'connection', JSON.stringify(err));
reject(err)
} else {
connection.query(sql, values, (err, rows) => {
if (err) {
logger.logInfo('-', 'err', 'query', JSON.stringify(err));
reject(err)
} else {
logger.logInfo('-', 'rows', 'query', JSON.stringify(rows));
resolve(rows)
}
connection.release();
})
}
})
})
}
}
let DoSql = new Mysql();
export default DoSql;
使用方法:
let sql = "SELECT `p_id` FROM `user_information_table` WHERE `p_name` = ?";
let result = await DoSql.query('db2', sql, ['小雅xx']);
配置 Redis
redis 是我们经常使用的缓存手段之一。
// redis.js
import { config } from '../config/config.js';
import redis from 'redis';
var RDS_PORT = config['redis'].port; //端口号
var RDS_HOST = config['redis'].ip;
var RDS_OPTS = {
auth_pass: config['redis'].auth,
db: config['redis'].db,
};
var client = redis.createClient(RDS_PORT, RDS_HOST, RDS_OPTS);
function redisHGet(map, key) {
return new Promise((resolve, reject) => {
client.hget(map, key, function (err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
});
})
}
.......略
使用方式:
let nowTime = new Date().getTime();
await redisHSet('socket', 'readnesstime', nowTime);
let mapValue = await redisHGet('socket', 'readnesstime');
在线上运行
首先,得有一个k8s集群,找了个文档,由于这里我们已经有了集群,所以直接下一步。
blog.csdn.net/u010039418/…
创建一个 node + tengine + centos 的基础镜像
这个镜像是基于 centos 的,所以我们只要再将 node 和 tengine 的环境搭出来就可以了。
FROM centos:7.6.1810
ENV TZ=Asia/Shanghai
COPY source/node-v10.13.0-linux-x64.tar.gz /home/aiyong/source/
#set -ex作用就是,当下面的命令执行出错后,就退出执行,不在继续往下执行。
RUN set -x \
\
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \
&& yum install -y wget \
&& mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup \
&& wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.cloud.aliyuncs.com/repo/Centos-7.repo \
&& wget -O /etc/yum.repos.d/epel.repo http://mirrors.cloud.aliyuncs.com/repo/epel-7.repo \
&& yum clean all \
&& yum makecache \
&& yum update -y \
&& yum install -y \
git gcc automake autoconf libtool make postgresql-devel \
pcre-devel openssl openssl-devel readline-devel bind-utils traceroute mlocate lrzsz openssl openssl-libs \
openssl-devel unzip screen optipng libjpeg-turbo-utils gifsicle libmcrypt libmcrypt-devel xvfb libXfont \
libxml2 libxml2-devel libcurl libcurl-devel libjpeg-turbo libjpeg-turbo-devel libpng libpng-devel bison \
logrotate supervisor psmisc net-tools ImageMagick xorg-x11-server-Xvfb.x86_64 pango-devel libldap freetype \
freetype-devel openldap openldap-devel re2c file glibc-static libstdc++-static cyrus-sasl-plain cyrus-sasl \
cyrus-sasl-devel cyrus-sasl-lib nscd \
&& yum clean all \
&& mkdir -p /home/aiyong/source \
# 安装 tengine 环境
&& cd /home/aiyong/source \
&& git clone https://github.com/simpl/ngx_devel_kit \
# && git clone https://github.com/openresty/lua-nginx-module \
&& git clone https://github.com/openresty/set-misc-nginx-module \
&& git clone https://github.com/openresty/redis2-nginx-module \
&& git clone https://github.com/openresty/echo-nginx-module \
&& git clone https://github.com/openresty/ngx_postgres \
&& wget http://tengine.taobao.org/download/tengine-2.3.0.tar.gz \
&& tar zxvf tengine-2.3.0.tar.gz \
&& cd tengine-2.3.0 \
&& ./configure --prefix=/usr/local/webserver/tengine --pid-path=/usr/local/webserver/tengine/nginx.pid --with-http_stub_status_module --with-ld-opt=-Wl,-rpath,/usr/lib --with-http_ssl_module --with-http_v2_module --add-module=/home/aiyong/source/ngx_devel_kit --add-module=/home/aiyong/source/redis2-nginx-module --add-module=/home/aiyong/source/echo-nginx-module --add-module=/home/aiyong/source/set-misc-nginx-module --with-ipv6 --add-module=/home/aiyong/source/ngx_postgres \
# --add-module=/home/aiyong/source/lua-nginx-module
&& make -j2 \
&& make install \
# 安装nodjs 环境
&& cd /home/aiyong/source \
&& tar zxvf node-v10.13.0-linux-x64.tar.gz \
&& cd node-v10.13.0-linux-x64 \
&& ln -s /home/aiyong/source/node-v10.13.0-linux-x64/bin/node /usr/local/bin \
&& ln -s /home/aiyong/source/node-v10.13.0-linux-x64/bin/npm /usr/local/bin \
&& node -v \
# 这里输出了那就是成功了
&& echo "alias ll='ls -l'" >> /root/.bashrc \
&& echo "alias vim='vi'" >> /root/.bashrfc
RUN set -x \
\
&& mkdir -p /data/syslog/ \
&& echo "1" > /data/syslog/nodjs.log \
&& groupadd -f nginx \
&& useradd -s /sbin/nologin -g nginx nginx \
&& mkdir /var/log/nginx \
&& chown nginx.nginx /var/log/nginx \
&& chmod a+w /var/log \
&& chmod a+w /var/log/nginx \
&& cp /etc/cron.daily/logrotate /etc/cron.hourly
#logrotate是个十分有用的工具,它可以自动对日志进行截断(或轮循)、压缩以及删除旧的日志文件
COPY biz /etc/logrotate.d/
#nignx 配置
COPY nginx.conf /usr/local/webserver/tengine/conf/
EXPOSE 80 443
#CMD ["supervisord" ,"-n", "-c", "/home/supervisord.conf"]
CMD ["/sbin/init"]
基础镜像搭好后,创建 nodejs 项目的 Dockerfile。
FROM node-tengine-centos:latest
WORKDIR /data/srv/{projectName}
COPY vhost /usr/local/webserver/tengine/conf/vhost/
COPY code /data/srv/{projectName}/
COPY block /usr/local/webserver/tengine/conf/block/
COPY conf /data/conf/
COPY nginx.conf /usr/local/webserver/tengine/conf/
RUN mkdir -p /data/srv/{projectName}/logs \
&& yum install -y logrotate supervisor psmisc net-tools \
&& chmod 777 /data/srv/{projectName}/logs \
&& mkdir -p /var/log/nginx \
&& chown nginx.nginx /var/log/nginx \
&& chmod a+w /var/log \
&& chmod a+w /var/log/nginx \
&& cd /data/srv/{projectName}/
&& npm install
EXPOSE 80
CMD ["supervisord" ,"-n", "-c", "/home/supervisord.conf"]
补充知识点
Supervisor(supervisord.org/)是用 Python 开发的一个 client/server 服务,是 Linux/Unix 系统下的一个进程管理工具,不支持 Windows 系统。它可以很方便的监听、启动、停止、重启一个或多个进程。用 Supervisor 管理的进程,当一个进程意外被杀死,supervisor 监听到进程死后,会自动将它重新拉起,很方便的做到进程自动恢复的功能,不再需要自己写 shell 脚本来控制。
这里的 conf 文件也是需要加上启动 node 项目的命令:
[supervisord]
nodaemon=true
logfile=/tmp/supervisord.log ;
[program:nginx]
command=/usr/local/webserver/tengine/sbin/nginx -g "daemon off;"
priority=100
stopsignal=QUIT
autostart=true ;
autorestart=true ;
[program:npm]
command=npm run start
autostart=true ;
autorestart=true ;
再来个简易的 nginx 配置:
server {
listen 80;
server_name x.x.com;
resolver x.x.x.x;
resolver_timeout 30s;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers Content-Type;
add_header Access-Control-Allow-Methods GET,POST;
add_header Access-Control-Allow-Credentials true;
error_log /var/log/nginx/error.log error;
access_log off;
client_max_body_size 4M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
deployment.yml:
deployment是用于管理pod的抽象层,它的定位类似于docker-compose。
k8s一个很巧妙的地方在于它把deployment层设计成“过程无关”的,你只需要声明你所期望的最终状态,k8s将会自动为你调度pod并保证它们满足你的预期。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: {k8sName}-deployment
namespace: xxxx #命名空间
spec:
template:
metadata:
labels:
app: {k8sName}
logisticType: consumer
spec:
dnsPolicy: "None"
dnsConfig:
nameservers:
- x.x.x.x
- x.x.x.x
options:
- name: timeout
value: "1"
- name: attempts
value: "2"
- name: rotate
- name: single-request-reopen
imagePullSecrets:
- name: registry-secret
containers:
- image: x.x.com/{dockerName} #镜像名
imagePullPolicy: Always
name: {k8sName}
env:
- name: SLS_LOG_TYPE
value: "socket-tengine-node"
resources:
requests:
cpu: 0.5
memory: "500Mi"
limits:
cpu: 4
memory: "12Gi"
---
apiVersion: v1
kind: Service
metadata:
name: {k8sName}-service
labels:
name: {k8sName}-service
namespace: xxxx #命名空间
spec:
type: ClusterIP
ports:
- port: 80
name: {k8sName}-http
targetPort: 80
selector:
app: {k8sName}
---
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: {k8sName}-hpa
namespace: xxxx #命名空间
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {k8sName}-deployment
minReplicas: 1
maxReplicas: 1
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 600
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {k8sName}-ingress
namespace: xxxx #命名空间
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: x.x.com #此service的访问域名
http:
paths:
- path: /
backend:
serviceName: {k8sName}-service
servicePort: 80
---