notes

技术细节

这篇主要对最近公司做的一个运维平台项目进行总结。主要是碰到的一些技术难点和项目中遇到的奇怪的问题。虽然项目中每个功能点都知道要怎么做,但是做的过程中也会遇到很多细节问题,需要一个个去解决,下面罗列出来。

如何获取终端命令输出的某一行某一列的内容

使用 awk 提取某一列,使用 sed 命令提取某一行。 awk 能很方便的处理每行格式都相同的文本,比如 CSV,一些终端命令的输出。几乎所有的 linux 系统都支持,mac 也支持。

基本用法是:

# 打印 docker ps 命令的第一列
docker ps | awk -F " " '{print $1}'

-F 参数指定了分隔符,这里以空格为分隔符, $1 表示分隔出来的第一个元素。

awk 还有一些内置的函数和控制语句,不过目前用不到,先不做了解。

# 打印 docker ps 命令的第一行
docker ps | sed -n '1p'

-n 表示不输出所有内容,根据后面的 ‘’ 里的条件输出,引号里面的 1 和 p ,1 表示第一行,p 表示输出。

那么结合一下 awk 和 sed 命令,就能获取到某一行某一列的值,例如获取第2行第3列的值:

docker ps | awk -F " " '{print $3}' | sed -n '2p'

比如用 kill 命令删除 3000 端口的进程:

kill -9 $(lsof -i:3000 | awk -F " " '{print $2}' | sed -n '2p')

在 docker 中,很多命令的输出很多都不是格式化的,很多列里面的值也带有空格,导致使用 $ 取某列值的时候就不准,但是还好 docker 的 –format 参数提供了基于 go 模版功能,可以定制输出的格式,如:

docker ps --format '##' | awk -F "##" '{print $2}'

使用 `` 获取当前对象,只取 name 和 status 的信息,输出的格式用 ## 隔开,然后用 awk 的 -F 参数就可以顺利的取到值。

使用 pkg 打包 node 应用,隐藏源码

执行 yarn add pkg 来安装 pkg 包。有一个需要注意的是使用 pkg 打包会把 node_modules 里面的文件也一起打包进去,如果项目中使用了 child_process 的话,而且在命令中使用了 node_modules 的一些模块的话,会报模块找不到的错误,这时除了打包后的 node 应用,还需要把 node_modules 也打包到镜像中。

使用 submodule 集成前端项目

项目前后端分离,后端项目在 CI 中打包需要把前端项目也打包进来,前端项目可以是单独的 docker 镜像,也可以直接让后端去拉取代码,这里使用的是后者。

以下是 drone CI 中的例子:

- name: frontend pull
  image: xxx/git
  commands:
    - git submodule update --init --recursive --remote
  depends_on:
    - clone

- name: frontend build
  image: xxx/node
  commands:
    - cd client
    - yarn
    - yarn build
  depends_on:
    - frontend pull

服务器磁盘满导致服务异常(所有接口无响应,但是容器还在跑)

如果真的遇到服务器磁盘满的情况,已经是救不回来了,只能实现预防,比如在上传文件和解压大文件的时候,先去判断一下磁盘空间大小,如果空间不够就返回一个错。上传文件时前端可以获取文件大小,后端先校验。解压文件时判断解压文件大小 * 4 倍的容量,只能预估一下大概的大小,不是很准确。

一些奇怪的问题

mongodb 密码中有特殊字符的解决办法

写在 docker-compose.yml 中的密码不用转义,连接的时候要根据客户端来定要不要转义,比如我使用的是 typeorm,在配置文件 ormconfig.json 文件中的密码是要转义后的。如果直接命令行连接上去,输入的密码是不需要转义的。

关于 docker in docker

这个项目中涉及到 dind(docker in docker),在 docker 容器中需要管理宿主机的容器。

我们用的是 node 做服务端。解决方式是自己做一个镜像,镜像内包含了 node,npm,pm2,docker,docker-compose 等必要的命令,然后把 /var/run/docker.sock 文件挂载到容器中,/var/run/docker.sock 文件是 docker 客户端和 docker 服务端通信的中间文件,客户端往这个文件中写入数据就能被服务端读取到。

关于 docker in docker 的 volume

dind 有一个坑,就是 docker 容器如果使用了 volume 挂载,比如我们在容器内使用 docker-compose up -d 启动容器,如果某些容器指定了 volume 字段,则 volume 左边的挂载路径要写宿主机的路径,不能是容器内的文件路径。参考[这篇文章](https://stackoverflow.com/questions/31381322/docker-in-docker-cannot-mount-volume)。

关于多个 docker 容器共享目录

多个 docker 共享文件夹的写法

# docker-compose.yml

version: '3.4'
services:
  app:
    image: server:v1.0
    ports:
      - 3000:3000
    volumes:
      - client:/app/client
      - .env:/app/.env

  nginx:
    image: nginx:latest
    ports:
      - 80:80
    volumes:
      - client:/usr/share/nginx/html

volumes:
  client:

关于 docker no space left on device

这个是磁盘空间不够的报错,解决方式有两种,一种是删除没有使用的镜像和容器,还有一种是加磁盘。 目前解决方案是删除不使用的镜像和容器,一行命令就可以了:docker system prune 如果解决方式是扩展硬盘容量的话,可能可以参考这篇文章

容器内 wget 宿主机不通

遇到的一个怪问题就是,我在容器内 ping 宿主机是通的,但是使用 wget 宿主机的内网ip发现不通,但是 wget 内网中的其他服务器却是可以的。

后来发现是防火墙的问题,容器网络如果是 bridge 模式的话,容器和宿主机之间的通信是通过 docker0 虚拟网桥实现的。容器内的报文通过 docker0 转发。当容器和宿主机通信时,docker0 网桥直接将报文转发到宿主机。报文的源地址是 docker0 网段的地址,而如果容器访问宿主机以外的机器,docker 的 SNAT 网桥会将报文的源地址转换微宿主机的地址,通过宿主机的网卡向外发送。

因此,当容器访问宿主机时,如果宿主机服务端口被防火墙拦截,那就无法联通宿主机,出现 Host is unreachable 的错误,而访问宿主机外的其他机器,由于报文地址是宿主机的地址,因此,不会被防火墙拦截。

解决方式有两种:

  1. 修改 /etc/firewalld/zones/public.xml 文件: ```
再重启防火墙

service firewalld restart


2. 或者使用命令

firewall-cmd –permanent –zone=trusted –change-interface=docker0


把 docker0 添加到信任的网桥里面。

注意:这里的 `docker0` 只是一个例子,到时候如果创建了自己的网段,则要把网段的那个名称写进去,比如 `br-u123giu21i`。

## 重启node容器的时候,内存和CPU飙涨,CPU涨到100%以上,内存使用到达1~2个G,这时pm2又会重启容器里面的node进程,经过多次重启后会稳定下来。

这是因为node中使用了 `staticCache` 模块,导致启动服务时需要把 `staticCache` 指定的目录下的所有文件都加载到内存里。

## 貌似是一个循环引用的问题,deploy.ts 引用了一个常量,只要使用了这个常量,emit出去的消息,监听者都收不到

确定是循环引用的问题,两个相互引用的模块A和B,如果先引入B,则在执行B中的require(A)时阻塞,进到A模块,再执行到A模块中的require(B)时,node运行时判断了是相互引用,则会返回一个空对象给A,此时A继续往下执行,但此时A中拿到的B是一个空对象。

## volume 文件夹太大导致启动容器时CPU和内容使用量暴涨

经查询,是不当使用 `staticCache` 模块导致的问题。

## docker-compose 很多命令的输出都是在stderr上面的,导致不好排查真正的错误

查了一圈,没有什么好的解决办法,docker-compose 就是把很多命令的输出放在 stderr 里的,要么就是使用原生的 docker 命令去操作。


## 缓存 index.html 页面

nginx 对 index.html 单独加上配置:

location =/ { add_header Cache-Control ‘no-cache’; … } ```