0. 写在前面:为什么你需要“神器”而非“常用命令
大家好,欢迎来到干货、技术、专业全方位遥遥领先的老杨的博客.
帮老杨点赞、转发、在看以及打开小星标哦
攒今世之功德,修来世之福报
那么容器化是不是意味着性能会显著提升?
前段时间和几个同行吐槽,他们花了大半年时间把公司的应用都容器化了,本想着性能会有提升,结果却发现不少应用跑得比以前还慢。这让我想起了自己刚接触容器技术时的困惑,今天就来聊聊这个话题。
其实这种情况挺普遍的,不是你一个人遇到。我记得几年前第一次把一个老项目迁移到Docker上时,也遇到过类似的问题。当时那种失望的感觉,就像你买了辆新车,结果发现还没有原来的电动车跑得快。
我们先用数据说话。拿一个简单的Web应用来测试,看看容器化前后的差异:
直接在服务器上跑的时候:
$ ab -n 10000 -c 100 http://localhost:8080/api/test结果显示:
Requests per second: 802.84 [#/sec] (mean)
Time per request: 124.56 [ms] (mean)
Transfer rate: 226.58 [Kbytes/sec] received容器化之后:
$ docker run -d -p 8080:8080 myapp:latest
$ ab -n 10000 -c 100 http://localhost:8080/api/test结果却变成了:
Requests per second: 595.68 [#/sec] (mean)
Time per request: 167.89 [ms] (mean)
Transfer rate: 168.12 [Kbytes/sec] received看到没有?性能直接掉了25%,这谁能接受啊。当时我也是懵的,明明大家都说容器化能提升效率,怎么到我这里就反了?
经过这些年的摸索,我发现问题主要出在几个地方。
虽然容器比虚拟机轻量很多,但毕竟还是多了一层抽象。就像你穿了一件薄外套,虽然不重,但总归还是增加了负担。
我经常用这个命令来监控容器的资源使用情况:
$ docker stats --no-stream输出结果:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
d4c2a8b9f1e3 myapp 15.67% 512MiB / 2GiB 25.60% 1.2MB / 890kB 45.2MB / 12MB同样的应用,直接在宿主机上运行时CPU使用率只有12.3%,这3%的差异看似不大,但在高并发场景下就会被放大。
容器的网络架构就像一座复杂的立交桥,数据包要绕好几个弯才能到达目的地。我之前专门测试过网络性能的差异:
宿主机直接访问:
$ iperf3 -c target-server -t 30结果:
[ ID] Interval Transfer Bandwidth Retr
[ 4] 0.00-30.00 sec 3.28 GBytes 939 Mbits/sec 0容器网络:
$ docker run --rm -it networkstatic/iperf3 -c target-server -t 30结果:
[ ID] Interval Transfer Bandwidth Retr
[ 4] 0.00-30.00 sec 2.89 GBytes 827 Mbits/sec 3网络性能掉了12%,这在网络密集型应用中影响就很明显了。
Docker的分层文件系统设计很巧妙,但也带来了额外的开销。我做过一个简单的磁盘性能测试:
宿主机直接写入:
$ dd if=/dev/zero of=testfile bs=1M count=1024 conv=fdatasync结果:
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 2.34567 s, 458 MB/s容器内写入:
$ docker run --rm -v /tmp/container-test:/data alpine sh -c \
"dd if=/dev/zero of=/data/testfile bs=1M count=1024 conv=fdatasync"结果:
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 2.89234 s, 371 MB/s存储性能下降了19%,这对于IO密集型应用来说就是致命的。
很多人容器化的时候都会设置资源限制,这本身是好事,但如果设置不当就会成为性能瓶颈。
$ docker run -d --memory=1g --name limited-app myapp:latest我见过不少人为了"安全"把内存限制设得特别小,结果应用频繁触发内存回收,性能自然就上不去。可以通过这个命令查看内存使用情况:
$ docker exec limited-app cat /proc/meminfo | grep MemAvailable
MemAvailable: 876540 kB如果这个值经常接近0,那基本可以确定是内存限制太严格了。
CPU限制更复杂一些:
$ docker run -d --cpus="0.5" --name cpu-limited myapp:latest这里设置了0.5个CPU核心,但很多人不知道这个限制是怎么工作的。可以通过cgroups查看具体设置:
$ watch "cat /sys/fs/cgroup/cpu/docker/$(docker inspect --format='{{.Id}}' cpu-limited)/cpu.cfs_quota_us"
50000这个数字表示在每个调度周期内,容器最多能使用50000微秒的CPU时间。
我之前做过一个对比,同样的应用用不同的基础镜像:
Alpine版本的Dockerfile:
FROM alpine:3.16
RUN apk add --no-cache python3 py3-pip
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python3", "app.py"]Ubuntu版本:
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y python3 python3-pip
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python3", "app.py"]结果镜像大小差异很大:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp-alpine latest a1b2c3d4e5f6 2 hours ago 87.3MB
myapp-ubuntu latest f6e5d4c3b2a1 2 hours ago 312MB镜像大小直接影响启动时间和内存占用,这个影响比你想象的要大。
很多人写Dockerfile的时候不注意层的优化,导致每次构建都很慢:
优化前:
FROM node:16
COPY . /app
WORKDIR /app
RUN npm install
RUN npm run build
CMD ["npm", "start"]优化后:
FROM node:16
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "start"]看起来差不多,但实际构建时间差异很大。当你只修改了代码而没有改依赖时,优化后的版本可以跳过npm install这一步,能节省大量时间。
如果你用的是K8s,那问题可能更复杂。我经常用这个命令查看Pod的调度情况:
$ kubectl describe pod myapp-pod-12345输出会显示:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m default-scheduler Successfully assigned default/myapp-pod-12345 to node-worker-03
Normal Pulling 2m kubelet Pulling image "myapp:latest"
Normal Pulled 1m kubelet Successfully pulled image "myapp:latest" in 45.67s
Normal Created 1m kubelet Created container myapp
Normal Started 1m kubelet Started container myapp从调度到启动,整个过程用了近2分钟,这在高频部署的场景下就很要命了。
如果你还用了Istio这样的服务网格,性能开销就更明显了。我测试过同一个服务,没有服务网格时的响应时间:
$ curl -o /dev/null -s -w "Time: %{time_total}s\n" http://service-a:8080/api/data
Time: 0.045s启用Istio后:
$ curl -o /dev/null -s -w "Time: %{time_total}s\n" http://service-a:8080/api/data
Time: 0.089s延迟直接翻倍,这还只是简单请求,复杂的调用链延迟会更严重。
遇到性能问题别慌,按步骤排查就行。
进入容器看看资源使用情况:
$ docker exec -it myapp-container top输出:
top - 14:23:45 up 2 days, 3:45, 0 users, load average: 0.45, 0.67, 0.89
Tasks: 8 total, 1 running, 7 sleeping, 0 stopped, 0 zombie
%Cpu(s): 12.3 us, 2.1 sy, 0.0 ni, 84.6 id, 0.8 wa, 0.0 hi, 0.2 si, 0.0 st
MiB Mem : 1024.0 total, 234.5 free, 567.2 used, 222.3 buff/cache重点看CPU的wa(等待IO的时间)和内存的使用情况。
$ docker exec myapp-container netstat -tulpnProto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 1/node
tcp 0 0 0.0.0.0:9090 0.0.0.0:* LISTEN 1/node如果Recv-Q和Send-Q经常有积压,那就是网络处理不过来了。
发现内存不够用时,可以动态调整:
# 查看当前使用情况
$ docker stats --no-stream myapp-container
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM %
a1b2c3d4e5f6 myapp-container 8.45% 756MiB / 1GiB 75.6%
# 调整内存限制
$ docker update --memory=1.5g myapp-container
# 再次查看
$ docker stats --no-stream myapp-container
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM %
a1b2c3d4e5f6 myapp-container 6.23% 756MiB / 1.5GiB 50.4%内存压力缓解后,CPU使用率也会降下来。
对于Java应用,JVM参数调优很重要:
FROM openjdk:11-jre-slim
COPY app.jar /app.jar
ENTRYPOINT ["java", \
"-XX:+UseG1GC", \
"-XX:MaxGCPauseMillis=200", \
"-XX:+UseStringDeduplication", \
"-Xms512m", \
"-Xmx1024m", \
"-jar", "/app.jar"]Node.js应用也类似:
FROM node:16-alpine
ENV NODE_ENV=production
ENV NODE_OPTIONS="--max-old-space-size=1024 --optimize-for-size"
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "app.js"]引入Redis缓存能显著提升性能:
version: '3.8'
services:
app:
image: myapp:latest
depends_on:
- redis
environment:
- REDIS_URL=redis://redis:6379
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru我测试过,加缓存前后的性能差异:
# 无缓存
$ time curl http://localhost:8080/api/heavy-computation
real 0m2.345s
# 有缓存
$ time curl http://localhost:8080/api/heavy-computation
real 0m0.045s差异非常明显。
搭建监控系统是必须的:
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'docker-containers'
docker_sd_configs:
- host: unix:///var/run/docker.sock
relabel_configs:
- source_labels: [__meta_docker_container_name]
target_label: container_name查询关键指标:
$ curl 'http://prometheus:9090/api/v1/query?query=rate(container_cpu_usage_seconds_total[5m])'groups:
- name: container-performance
rules:
- alert: HighContainerCPU
expr: rate(container_cpu_usage_seconds_total[5m]) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Container CPU usage is high"
description: "Container {{$labels.container_name}} CPU usage is above 80%"这个技巧能大幅减小镜像体积:
# 构建阶段
FROM node:16 AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 运行阶段
FROM node:16-alpine AS runtime
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /build/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /build/package*.json ./
RUN npm ci --only=production && npm cache clean --force
USER nodejs
CMD ["node", "dist/index.js"]镜像大小对比:
$ docker images
REPOSITORY TAG SIZE
myapp-single-stage latest 1.2GB
myapp-multi-stage latest 156MB启动速度提升明显。
对于网络密集型应用,可以考虑使用host网络:
$ docker run -d --network=host --name fast-app myapp:latest性能测试对比:
# 桥接网络
$ docker run --rm --network=bridge nicolaka/netshoot \
iperf3 -c target-server -t 10
[ ID] Interval Transfer Bandwidth Retr
[ 4] 0.00-10.00 sec 756 MBytes 634 Mbits/sec 12
# 主机网络
$ docker run --rm --network=host nicolaka/netshoot \
iperf3 -c target-server -t 10
[ ID] Interval Transfer Bandwidth Retr
[ 4] 0.00-10.00 sec 945 MBytes 793 Mbits/sec 3性能提升25%,重传次数也大幅减少。
我写了个脚本来定期做基准测试:
#!/bin/bash
# performance-benchmark.sh
echo "=== Container Performance Benchmark ==="
echo "测试时间: $(date)"
echo -e "\n=== CPU性能测试 ==="
docker exec myapp-container sysbench cpu --threads=4 --time=30 run
echo -e "\n=== 内存性能测试 ==="
docker exec myapp-container sysbench memory --memory-total-size=1G --time=30 run
echo -e "\n=== 网络性能测试 ==="
docker exec myapp-container curl -o /dev/null -s -w "下载速度: %{speed_download} bytes/sec, 总时间: %{time_total}s\n" http://speedtest.server/100MB.bin经过这几年的实践,我觉得容器化性能问题其实是可以解决的,关键在于要有系统性的思维。
容器化不是银弹,它带来便利的同时也会引入一些开销。但只要我们理解这些开销的来源,采用正确的优化策略,完全可以把性能损失降到最低,甚至在某些场景下还能获得性能提升。
这里老杨先声明一下,日常生活中大家都叫老杨波哥,跟辈分没关系,主要是岁数大了.就一个代称而已. 老杨的00后小同事老杨喊都是带哥的.张哥,李哥的. 但是这个称呼呀,在线下参加一些活动时.金主爸爸也这么叫就显的不太合适. 比如上次某集团策划总监,公司开大会来一句:“今个咱高兴!有请IT运维技术圈的波哥讲两句“ 这个氛围配这个称呼在互联网这行来讲就有点对不齐! 每次遇到这个情况老杨就想这么接话: “遇到各位是缘分,承蒙厚爱,啥也别说了,都在酒里了.老杨干了,你们随意!” 所以以后咱们改叫老杨,即市井又低调.还挺亲切,老杨觉得挺好.
运维X档案系列文章:
企业级 Kubernetes 集群安全加固全攻略( 附带一键检查脚本)
看完别走.修行在于点赞、转发、在看.攒今世之功德,修来世之福报
老杨AI的号: 98dev