Chemmy's Blog

chengming0916@outlook.com

1. 第一个Qt程序

1.1 Hello Qt

1
2
3
4
5
6
7
8
9
10
#include <QApplication>          // 头文件引用, 每个Qt类都有对应的头文件,类的定义
#include <QLabel> // 包含了对该类的定义

int main(int argc, char *argv[])
{
QApplication app(argc, argv); // 创建QApplication对象,管理整个应用
QLabel *label = new QLabel("Hello Qt"); // QLabel部件,用于显示
label->show(); // # 使QLabel部件可见
return app.exec(); // 将应用程序的控制权传递给Qt,程序等待用户操作
}

测试程序

图片占位

在源码根目录打开命令提示符执行qmake -project生成hello.pro项目文件,然后执行qmake hello.pro从这个项目文件生成makefile文件,在输入make命令就可以构建该应用。

方式一、虚拟ip访问

安装docker时,docker会默认创建一个内部的桥接网络docker0,每创建一个容器分配一个虚拟网卡,容器之间可以根据ip互相访问。

1
2
3
4
5
6
7
8
9
10
11
# ifconfig
---
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 0.0.0.0
inet6 fe80::42:35ff:feac:66d8 prefixlen 64 scopeid 0x20<link>
ether 02:42:35:ac:66:d8 txqueuelen 0 (Ethernet)
RX packets 4018 bytes 266467 (260.2 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4226 bytes 33935667 (32.3 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
---

运行一个centos镜像, 查看ip地址得到:172.17.0.7

1
2
3
4
5
6
7
8
9
10
11
12
13
# docker run -it --name centos-1 docker.io/centos:latest
# ifconfig

---
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.7 netmask 255.255.0.0 broadcast 0.0.0.0
inet6 fe80::42:acff:fe11:7 prefixlen 64 scopeid 0x20<link>
ether 02:42:ac:11:00:07 txqueuelen 0 (Ethernet)
RX packets 16 bytes 1296 (1.2 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 8 bytes 648 (648.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
---

以同样的命令再起一个容器,查看ip地址得到:172.17.0.8

1
2
3
4
5
6
7
8
9
10
11
12
13
# docker run -it --name centos-2 docker.io/centos:latest
# ifconfig

---
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.8 netmask 255.255.0.0 broadcast 0.0.0.0
inet6 fe80::42:acff:fe11:8 prefixlen 64 scopeid 0x20<link>
ether 02:42:ac:11:00:08 txqueuelen 0 (Ethernet)
RX packets 8 bytes 648 (648.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 8 bytes 648 (648.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
---

容器内部ping测试结果如下:

1
2
3
4
5
6
7
8
9
# ping 172.17.0.7

---
PING 172.17.0.7 (172.17.0.7) 56(84) bytes of data.
64 bytes from 172.17.0.7: icmp_seq=1 ttl=64 time=0.205 ms
64 bytes from 172.17.0.7: icmp_seq=2 ttl=64 time=0.119 ms
64 bytes from 172.17.0.7: icmp_seq=3 ttl=64 time=0.118 ms
64 bytes from 172.17.0.7: icmp_seq=4 ttl=64 time=0.101 ms
---

这种方式必须知道每个容器的ip,在实际使用中并不实用。

运行容器的时候加上参数link

运行第一个容器

1
docker run -it --name centos-1 docker.io/centos:latest

运行第二个容器

1
2
3
docker run -it --name centos-2 \ 
--link centos-1:centos-1 \
docker.io/centos:latest

--link:参数中第一个centos-1是 容器名 ,第二个centos-1是定义的 容器别名 (使用别名访问容器),为了方便使用,一般别名默认容器名。

测试结果如下:

1
2
3
4
5
6
7
8
9
# ping centos-1

---
PING centos-1 (172.17.0.7) 56(84) bytes of data.
64 bytes from centos-1 (172.17.0.7): icmp_seq=1 ttl=64 time=0.210 ms
64 bytes from centos-1 (172.17.0.7): icmp_seq=2 ttl=64 time=0.116 ms
64 bytes from centos-1 (172.17.0.7): icmp_seq=3 ttl=64 time=0.112 ms
64 bytes from centos-1 (172.17.0.7): icmp_seq=4 ttl=64 time=0.114 ms
---

此方法对容器创建的顺序有要求,如果集群内部多个容器要互访,使用就不太方便。

方式三、创建bridge网络

  1. 安装好docker后,运行如下命令创建bridge网络:
1
docker network create testnet

查询到新创建的bridge testnet。

  1. 运行容器连接到testnet网络。
    使用方法:
    1
    2
    3
    docker run -it --name <容器名> \
    --network <bridge> \
    --network-alias <网络别名> <镜像名>
1
2
3
4
docker run -it --name centos-1 --network testnet \
--network-alias centos-1 docker.io/centos:latest
docker run -it --name centos-2 --network testnet \
--network-alias centos-2 docker.io/centos:latest
  1. 从一个容器ping另外一个容器,测试结果如下:
1
2
3
4
5
6
7
8
9
# ping centos-1

---
PING centos-1 (172.20.0.2) 56(84) bytes of data.
64 bytes from centos-1.testnet (172.20.0.2): icmp_seq=1 ttl=64 time=0.158 ms
64 bytes from centos-1.testnet (172.20.0.2): icmp_seq=2 ttl=64 time=0.108 ms
64 bytes from centos-1.testnet (172.20.0.2): icmp_seq=3 ttl=64 time=0.112 ms
64 bytes from centos-1.testnet (172.20.0.2): icmp_seq=4 ttl=64 time=0.113 ms
---
  1. 若访问容器中服务,可以使用这用方式访问 <网络别名>:<服务端口号>

推荐使用这种方法,自定义网络,因为使用的是网络别名,可以不用顾虑ip是否变动,只要连接到docker内部bright网络即可互访。bridge也可以建立多个,隔离在不同的网段。

准备

镜像选择

quay.io/coreos/etcd:3.2.7
bitname/etcd

创建ETCD数据目录

1
2
3
# data 存储容器持久化数据
# config 存储容器使用的配置文件
mkdir -p /usr/local/docker/etcd/{data,config}

创建ETCD配置文件

配置文件路径为 /usr/local/docker/etcd/config/etcd.config.yml

1
2
3
4
5
6
7
8
9
10
11
12
name: etcd # etcd member 名称,可根据实际情况修改
data-dir: /var/etcd #etcd 数据目录,可根据实际情况修改
listen-client-urls: http://0.0.0.0:2379 #client 流量监听地址,没特殊需求按文档填写即可
advertise-client-urls: http://0.0.0.0:2379 # 该 member 向外部通告的客户端 url 列表,单节点部署时不需要修改,集群部署模式需修改为容器所在节点对外提供服务的 IP
listen-peer-urls: http://0.0.0.0:2380 # peer 流量监听地址,没特殊需求按文档填写即可
initial-advertise-peer-urls: http://0.0.0.0:2380 # 该 member 向同一集群内其他 member 通告的 peer url 列表,单节点部署时不需要修改,集群部署模式需修改为容器所在节点对外提供服务的 IP
initial-cluster: etcd=http://0.0.0.0:2380 # 初始化集群节点信息,单节点部署时不需要修改,集群部署模式需要填写集群中所有 member 的信息
initial-cluster-token: etcd-cluster # 初始化集群时使用的 token,随便写
initial-cluster-state: new # 初始化集群状态,可选的值为 **new** 或者 **existing**,通常采用 **new**
logger: zap
log-level: info
# log-outputs: stderr

创建并启动ETCD服务

1
docker run -d --name etcd -p 2379:2379 -p 2380:2380 -v /usr/local/docker/etcd/data:/var/etcd -v /usr/local/docker/etcd/config:/var/lib/etcd/config quay.io/coreos/etcd: 3.5.12 

使用docker-compose部署

创建Docker-compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
version: '3'

services:
etcd:
container_name: etcd
image: quay.io/coreos/etcd:v3.5.12
command: /usr/local/bin/etcd --config-file=/var/lib/etcd/config/etcd.conf.yml
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/data:/var/lib/etcd
- ${DOCKER_VOLUME_DIRECTORY:-.}/config/etcd.config.yml:/var/lib/etcd/conf/etcd.conf.yml
ports:
- 2379:2379
- 2380:2380
restart: always

networks: # 船舰一个新的bridge模式网络,名称 etcd-tier,名称可以根据需求自定义
default:
name: etcd-tier
driver: bridge

基于环境变量配置, 配置参考配置文件方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
version: '3'

services:
etcd:
container_name: etcd
image: quay.io/coreos/etcd:v3.5.12
environment:
- ETCD_NAME=etcd
- ETCD_DATA_DIR=
- ETCD_LISTEN_CLIENT_URLS=
- ETCD_ADVERTISE_CLIENT_URLS=
- ETCD_LISTEN_PEER_URLS=
- ETCD_INSTALL_ADVERTISE_PEER_URLS=
- ETCD_INSTALL_CLUSTER_TOKEN=
- ETCD_INSTALL_CLUSTER
- ETCD_INSTALL_CLUSTER_STATE=new
- ETCD_LOGGER=zap
- ETCD_LOG_LEVEL=info
- ALLOW_NONE_AUTHENTICATION="yes" # 允许无身份验证访问
- TZ="Asia/Shanghai"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:=.}/data:/var/etcd
- /etc/localtime:/etc/localtime:rw
ports:
- 2379:2379
- 2380:2380
restart: always
networks:
default:
name: etcd-tier
driver: bridge

创建并启动etcd

1
2
cd {docker-compose文件所在目录}
docker compose up -d

测试命令

1
2
3
4
5
6
7
8
9
10
11
12
etcdctl --endpoints=192.168.1.2:2379 --write-out=table endpoint health

etcdctl --endpoints=192.168.1.2:2379 --write-out=table endpoint status

# 查看member状态
etcdctl --endpoints=192.168.1.2:2379 --write-out=table member list

# 写入数据
etcdctl --endpoints=192.168.1.2:2379 put foo bar

# 读取数据
etcdctl --endpoints=192.168.1.2:2379 get foo

获取VideoCapture实例

1
2
3
4
5
6
7
8
# 读取视频流
strem_capture = cv2.VideoCapture("rtst://192.168.0.0/live/demo")

# 读取视频文件
file_capture = cv2.VideoCapture('demo.mp4')

# 读取摄像头
capture = cv2.VideoCapture(0)

获取摄像头编号可使用ls -al /dev/ |grep video,输出信息以video开头其后缀为数字即为可能的摄像头编号。

检查获取VideoCapture实例是否成功

1
2
3
# 校验获取VideoCapture类实例
if not capture.isOpened():
return

获取视频流信息

1
2
3
4
5
6
7
8
# 获取视频帧的宽
width = capture.get(cv2.CAP_PROP_FRAME_WIDTH)

# 获取视频帧的高
height = cpature.get(cv2.CAP_PROP_FRAME_HEIGHT)

# 获取视频帧率
fps = capture.get(cv2.CAP_PROP_FPS)

获取帧画面

1
success, frame = capture.read()

当需要同时处理多路摄像头时一般使用grab()retrieve()代替

1
2
3
4
5
6
7
success_1 = capture.grab()
success_2 = stream_capture.grab()

if success_1 and success_2:
frame_1 = capture.retrieve()
frame_2 = stream_capture.retrieve()

设置分辨率

1
2
3
4
5
# 设置摄像头分辨率的宽
capture.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)

# 设置摄像头分辨率的高
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)

保存视频文件

无论是视频文件存储还是摄像头画面保存都是用VideoWriter类,初始化时需要传入文件名(包含文件格式)、视频编解码器、视频保存帧率 、分辨率,保存视频的帧率最好和读入的帧率一致,分辨率可以更改,只是要求写入的帧大小要与分辨率保持一致。
若指定的文件名已存在则会覆盖文件。

1
2
3
writer = cv2.VideoWriter('output.mp4', 
cv2.VideoWriter_fourcc(*'MP4V'), 30, (1080,1920))
writer.write(frame)

释放资源

不管是VideoCapture还是VideoWriter类,使用完都应该释放资源

1
2
3
4
5
# 释放VideoCapture资源
capture.release()

# 释放VideoWriter资源
writer.release()

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -*- coding: utf-8 -*-
# /usr/bin/env/python3

import cv2
import time

capture = cv2.VideoCapture('rtsp://192.168.0.0/live/demo)
fourcc = cv2.VideoWriter_fourcc(*'MP$v') # 或H264,H265
fps = capture.get(cv2.CAP_PROP_FPS)
width = capture.get(cv2.CAP_PROP_FRAME_WIDTH)
height = capture.get(cv2.CAP_PROP_FRAME_HEIGHT)

writer = cv2.VideoWriter('demo.mp4', fourcd, fps, (height, width))


while True:
if not capture.isOpened():
time.sleep(0.5)
continue
success, frame = capture.read()
if success:
cv2.imshow('DEMO', frame) # 显示画面
writer.write(frame) # 保存视频文件
if (cv2.waitKey(20) & 0xff) == ord('q'): # 等待20ms并判断是否按下'q'退出,waitkey只能传入整数,
break

capture.release() # 释放VideoCapture
writer.release() # 释放VideoWriter
cv2.destroyAllWindows() # 销毁所有opencv显示窗口

环境安装

此处使用设备 Tesla P40 + Debian 12为例

显卡驱动:下载 NVIDIA 官方驱动 | NVIDIA
CUDA Toolkit: CUDA Toolkit Archive | NVIDIA Developer
![[Docke种使用GPU运行Ollama/IMG-20260105103041635.png]]
注意CUDA版本对应,否则可能会导致CUDA在容器内无法运行

1
2
3
4
5
6
7
8
9
10
11
12
13
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt-get update

export NVIDIA_CONTAINER_TOOLKIT_VERSION=1.18.1-1
sudo apt-get install -y \
nvidia-container-toolkit=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
nvidia-container-toolkit-base=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
libnvidia-container-tools=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
libnvidia-container1=${NVIDIA_CONTAINER_TOOLKIT_VERSION}
  • 禁用 nouveau
    参考[[../Linux/Ubuntu禁用Nouveau驱动|Ubuntu禁用Nouveau驱动]]

  • 验证环境是否安装成功

    1
    docker run --rm --gpus all nvidia/cuda:12.2.0-runtime-ubuntu22.04 nvidia-smi

![[Docke种使用GPU运行Ollama/IMG-20260105103041714.png]]

直接运行Docker容器

1. 运行Ollama容器

1
docker run --gpus=all -d -e OLLAMA_MODEL:qwen2.5-coder:7b -e OLLAMA_ENV:production -v=qwen-coder:/root/.ollama -p 11437:11434 --name qwen-coder -d ollama/ollama:latest

2. 拉取模型并运行

1
ollama run qwen2.5-coder:7b

使用Docker Compose

1. 创建docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services:
ollama:
container_name: qwen2.5-coder-7b
image: ollama/ollama:latest
ports:
- 11434:11431
volumes:
- ./data:/root/.ollama
deploy:
resources:
devices:
- driver: nvidia
capabilities: ["gpu"]
environment:
OLLAMA_MODEL: "qwen2.5-coder:7b"
restart: unless-stopped

2. 启动服务

docker-compose.yml所在目录运行以下命令:

1
docker-compose up -d

验证是否使用GPU启动Ollama

1
2
docker exec -it <ollama容器ID> bash # 进入已启动的容器
ollama ps

如果PROCESSOR中显示为GPU则启动成功
![[Docke种使用GPU运行Ollama/IMG-20260105103041772.png]]

Ollama运行deepseek-r1示例

1
2
3
4
5
6
7
8
9
10
# CPU运行
docker run -d -v deepseek-r1:/root/.ollama -p 11434:11434 --name deepseek-r1 ollama/ollama:latest

# GPU运行
docker run -d -v deepseek-r1:/root/.ollama -p 11434:11434 --name deepseek-r1-gpu ollama/ollama:latest

# 如果多张显卡运行需要添加负载均衡
-e OLLAMA_NUM_GPU:2 # 启用GPU数量(需与CUDA_VISIBLE_DEVICES匹配)
-e OLLAMA_SCHED_SPREAD:1 # 自动负载均衡

注意事项

  • 使用_nvidia-smi_确认GPU是否被正确利用。
  • 如果仅需CPU支持,可移除_–gpus=all_或相关配置。
  • GPU模式下性能显著提升,但需确保驱动和CUDA版本兼容。

1. 创建黑名单配置文件

1
sudo nano /etc/modprobe.d/blacklist-nouveau.conf
  • 添加以下内容以禁用Nouveau:
1
2
blacklist nouveau
options nouveau modeset=0

2. 更新initramfs

  • 更新内核的initramfs文件以应用更改:
    1
    sudo update-initramfs -u

3. 重启系统

  • 执行以下命令重启系统:
1
sudo reboot

4. 验证禁用状态

  • 重启后,检查Nouveau模块是否已禁用:

    1
    lsmod | grep nouveau
  • 如果没有输出,说明Nouveau已成功禁用。

注意事项

  • 禁用Nouveau后,可以安装NVIDIA专有驱动以获得更好的性能。

  • 如果需要恢复Nouveau驱动,只需删除_/etc/modprobe.d/blacklist-nouveau.conf_文件并重新更新initramfs。

货币格式

1
2
3
4
5
<!-- 默认保留两位小数 输出: $12.34 -->
<TextBlock Text="{Binding Price, StringFormat={}{0:C}}" />

<!-- 保留一位小数 输出: $123.4 -->
<TextBlock Text="{Binding Price, StringFormat={}{0:C1}}" />

固定文字

1
2
3
4
5
6
<!-- 固定前缀 输出: 单价:$12.34 -->
<TextBlock Text="{Binding Price, StringFormat=单价: {0:C}}" />

<!-- 固定后缀 输出: 12.345元 -->
<TextBlock Text="{Binding Price, StringFormat={}{0}元}" />

数字格式化

1
2
3
4
5
6
7
8
9
10
11
<!-- 固定位数,仅支持整形 输出: 086723 -->
<TextBlock Text="{Binding Total, StringFormat={}{0:D6}}" />

<!-- 固定小数点后位数 输出: 8234.9354 -->
<TextBlock Text="{Binding Total, StringFormat={}{0:F4}}" />

<!-- 使用用分割符并指定小数点后位数 输出: 8234.933 -->
<TextBlock Text="{Binding Total, StringFormat={}{0:N3}}" />

<!-- 格式化百分比 输出: 78.9%-->
<TextBlock Text="{Binding Persent, StringFormat={}{0:P1}}" />

占位符

1
2
3
4
<!-- 输出: 0123.46 -->
<TextBox Text="{Binding Price, StringFormat={}{0:0000.00}}" />
<!-- 输出: 123.46 -->
<TextBox Text="{Binding Price, StringFormat={}{0:####.##}}" />

时间日期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- 输出: 5/4/2015 -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:d}}" />
<!-- 输出: Monday, May 04, 2015 -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:D}}" />
<!-- 输出: Monday, May 04, 2015 5:46 PM -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:f}}" />
<!-- 输出: Monday, May 04, 2015 5:46:56 PM -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:F}}" />
<!-- 输出: 5/4/2015 5:46 PM -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:g}}" />
<!-- 输出: 5/4/2015 5:46:56 PM -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:G}}" />
<!-- 输出: May 04 -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:m}}" />
<!-- 输出: May 04 -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:M}}" />
<!-- 输出: 5:46 PM -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:t}}" />
<!-- 输出: 5:46:56 PM -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:T}}" />
<!-- 输出: 2015年05月04日 -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:yyyy年MM月dd日}}" />
<!-- 输出: 2015-05-04 -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:yyyy-MM-dd}}" />
<!-- 输出: 2015-05-04 17:46 -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:yyyy-MM-dd HH:mm}}" />
<!-- 输出: 2015-05-04 17:46:56 -->
<TextBox Text="{Binding DateTimeNow, StringFormat={}{0:yyyy-MM-dd HH:mm:ss}}" />

多重绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--
\a &#x07; BEL
\b &#x08; BS - Backspace
\f &#x0c; FF - Formfeed
\n &#x0a; LF, NL - Linefeed, New Line
\r &#x0d; CR - Carriage return
\t &#x09; HT - Tab, Horizontal Tabelator
\v &#x0b; VT - Vertical Tabelator
-->
<TextBlock.Text>
<MultiBinding StringFormat="姓名: {0}&#x09;{1}">
<Binding Path="FirstName" />
<Binding Path="LastName" />
</MultiBinding>
</TextBlock.Text>

一、 背景与机遇:为什么需要热更新?

热更新指在不重新发布客户端安装包(APK/IPA)的情况下,通过下发补丁或脚本,在线修复Bug或更新部分功能。

核心驱动力

  1. 快速迭代与修复:传统应用商店审核周期长(早期App Store可达2周),无法满足紧急Bug修复、节日活动等快速上线需求。
  2. 提升用户体验:避免强制用户下载完整的安装包,降低更新成本,提高版本覆盖率。
  3. 业务灵活性:支持A/B测试、功能灰度发布等动态化运营策略。

技术机遇(iOS侧):2013年iOS 7引入的 JavaScriptCore 框架是关键转折点。它使得原生Objective-C与JavaScript之间的高效、无缝通信成为可能,为基于JS脚本的热更新方案(如JSPatch)铺平了道路。随着Swift的发布和iOS 7+用户成为主流,热更新技术开始爆发。

二、 iOS平台热更新方案详解

1. 基于JavaScriptCore的轻量级方案(非跨平台)

这类方案适合小范围Bug修复和逻辑更新,通过JS脚本调用/替换原生方法。

方案 脚本语言 特点 现状
Rollout.io JavaScript 支持OC和Swift,已平台化,有商业化产品使用。 成熟,持续维护。
JSPatch JavaScript 仅支持OC,在国内广泛使用(腾讯系等),平台化成熟。 成熟,但苹果审核政策收紧后使用需谨慎。
DynamicCocoa OC -> JS 滴滴内部方案。最大亮点:提供工具将OC代码转译为JS,无需手写JS;支持xib/storyboard、图片资源更新。 曾计划开源,需关注其后续动态。

共同原理:利用 JavaScriptCore 建立JS与OC的桥接,通过运行时(Runtime)动态替换方法实现(method swizzling)。

2. 跨平台应用开发方案

这类方案旨在用一套代码开发iOS、Android(及Web)应用,天然支持热更新。

方案 发起方 核心特点 性能与生态
React Native (RN) Facebook 使用React语法和JSX,重视各平台原生体验。 性能持续优化,生态庞大(Facebook、腾讯、京东等大量应用使用)。早期性能有槽点,现已大幅改善。
Weex 阿里巴巴 目标:一次编写,生成iOS、Android、Web三端代码。采用Vue.js语法。 经阿里系产品(如2016年双11)大规模验证。旨在解决RN代码复用率、性能优化被动等问题。

选型提示Weex 在代码复用和跨平台一致性上追求更极致;RN 则更强调遵循各平台原生设计规范,生态更成熟。

3. 其他方案
  • Wax:使用Lua作为脚本语言,性能优于JS。代表作《愤怒的小鸟》。后期由阿里接手维护,但支付宝后期转向了JSPatch。
  • Hybrid App:基于WebView(如PhoneGap)。缺点:性能较差,体验不及原生。随着RN/Weex的成熟,已不再是高性能App的首选。

关于脚本语言的思考

  • Lua:性能高,但在应用开发生态(类库、文档)上不如JS成熟,多用于游戏热更(Cocos2d-X, Unity3D)。
  • JavaScript:生态极其丰富,性能足以满足大部分应用开发场景,是应用端热更的主流选择。
4. 技术背景:GCC、LLVM与Clang
  • GCC:传统的编译器集合,早期OC的编译器。
  • LLVM/Clang:Apple主导,旨在取代GCC。Clang 是LLVM的前端,负责将OC代码编译成抽象语法树(AST)。
  • 关联DynamicCocoa 的高明之处在于直接利用 Clang 生成的AST进行解析和转译,从而实现了“用OC写补丁”的体验。

iOS热更新方案对比图

三、 Android平台热修复方案详解

Android方案主要围绕DEX补丁的生成、下发和加载展开。

方案 出品方 核心原理 优点 缺点
百川Hotfix 阿里巴巴 融合热部署(AndFix)与冷部署,支持类、资源、SO文件修复。 功能全面,可视化打补丁,接入简单。 -
Robust 美团 为每个方法插入“跳转”逻辑,通过改变跳转目标实现修复。 实时生效,兼容性高,稳定性好。 侵入打包流程,增加包体积和方法数。
Tinker 微信 全量替换DEX。通过自研DexDiff算法生成极小差量包。 补丁包小,功能强大(支持类、资源、SO)。 不实时生效,需重启。合并DEX时消耗内存/磁盘,可能失败。
QFix (类) 手机QQ空间 类似Google Multidex,将补丁作为新的DEX加载。 兼容性高,稳定性好。 不实时生效。Dalvik下性能问题;Art下补丁包大;需侵入打包。

Android热修复方案对比图

关键概念

  • 实时生效:补丁应用后立即起作用,无需重启App(如Robust、AndFix)。
  • 冷启动生效:补丁在下次App启动时生效(如Tinker、QFix)。
  • 侵入式打包:需要在正常打包流程中插入特定步骤,可能影响CI/CD流水线。

四、 总结与方案选型快速参考

平台 需求场景 推荐方案 关键考量
iOS 紧急Bug修复,小功能更新 JSPatch (需注意审核风险) 或评估 Rollout 苹果官方对热更新审核政策严格,需谨慎使用。
iOS 新功能模块,跨平台需求 React NativeWeex RN生态更成熟;Weex追求更高代码复用。
Android 紧急Bug修复,要求实时生效 Robust 平衡了实时性、兼容性和稳定性。
Android 常规版本更新,修复范围大 Tinker 补丁包小,功能全面,社区活跃。
Android 追求简单接入,功能全面 百川Hotfix 阿里系产品,提供平台化支持。

核心建议

  1. 明确需求:是修复致命Bug,还是实现功能动态化?是否需要跨平台?
  2. 评估成本:接入复杂度、包体积影响、对现有构建流程的侵入程度。
  3. 关注生态与合规:特别是iOS方案,需密切关注苹果的审核政策动向。
  4. 混合使用:大型App常采用混合策略,如基础模块用原生,高动态业务模块用RN/Weex。

五、 拓展资源

一、为什么需要日志系统?

1.1 日志的定义与挑战

日志是程序产生的、遵循特定格式(通常包含时间戳)的文本数据。在分布式系统中,日志通常分为:

  • 系统日志:操作系统和基础服务的运行状态
  • 应用日志:业务应用程序的输出
  • 安全日志:安全审计和访问记录

传统日志管理的痛点

  1. 分散存储:日志分布在多台服务器的不同文件中
  2. 查找困难:故障排查时需要登录多台服务器,使用 grep/sed/awk 等工具手动搜索
  3. 格式多样:不同应用使用不同的日志格式和滚动策略
  4. 实时性差:难以及时发现和响应系统问题

1.2 日志系统的价值

  • 故障排查:快速定位问题根源,减少平均修复时间(MTTR)
  • 系统监控:实时了解服务运行状态和性能指标
  • 安全审计:追踪异常访问和潜在安全威胁
  • 业务分析:基于日志数据进行用户行为分析和业务洞察

二、ELK Stack 基础架构

2.1 核心组件介绍

组件 角色 特点
Elasticsearch 分布式搜索引擎 高可扩展、高可靠、支持全文检索和结构化查询
Logstash 数据收集处理引擎 支持多种数据源、强大的过滤和转换能力
Kibana 数据可视化平台 丰富的图表类型、交互式仪表板
Filebeat 轻量级数据收集器 资源消耗低、专为日志收集优化

版本信息

  • Elasticsearch 5.2.2
  • Logstash 5.2.2
  • Kibana 5.2.2
  • Filebeat 5.2.2
  • Kafka 2.10

2.2 Logstash 数据处理流程

1
Input → Filter → Output
  • Input插件:支持 File、Stdin、TCP、Syslog、Redis、Collectd 等
  • Filter插件
    • grok:正则表达式解析
    • date:时间处理
    • json:JSON编解码
    • mutate:数据修改
  • Output插件:支持 Elasticsearch、Redis、TCP、File 等

Grok 示例

1
2
3
grok {
match => ["message", "(?m)\[%{LOGLEVEL:level}\] \[%{TIMESTAMP_ISO8601:timestamp}\] \[%{DATA:logger}\] \[%{DATA:threadId}\] \[%{DATA:requestId}\] %{GREEDYDATA:msgRawData}"]
}

三、架构演进:从简单到复杂

3.1 简单版架构(学习用)

1
应用服务器 → Logstash → Elasticsearch → Kibana

特点

  • Logstash 直接连接 Elasticsearch
  • 部署简单,适合学习和测试
  • 缺点:单点故障、资源消耗大、不适合生产环境

3.2 集群版架构

1
多台服务器 → Logstash Shipper → Elasticsearch集群 → Kibana

改进

  • Elasticsearch 集群化,提高可用性
  • 每台服务器部署 Logstash Agent
  • 问题
    • Logstash 消耗服务器资源
    • 大并发时可能丢失数据
    • 不支持多机房部署

3.3 引入消息队列架构

1
多台服务器 → Logstash Shipper → Kafka集群 → Logstash Indexer → Elasticsearch集群 → Kibana

关键改进

  • 引入 Kafka 作为消息队列,削峰填谷
  • 分离角色:Shipper(收集)和 Indexer(处理)
  • 选择 Kafka 而非 Redis 的原因
    • 数据可靠性:Kafka 保证可靠,Redis 可能丢失
    • 堆积能力:Kafka 依赖磁盘,Redis 依赖内存

3.4 多机房部署架构

1
2
机房A:应用 → Logstash → Kafka → Logstash → Elasticsearch → Kibana
机房B:应用 → Logstash → Kafka → Logstash → Elasticsearch → Kibana

单元化设计原则

  • 每个机房独立完整的日志处理链路
  • 避免跨机房数据传输
  • 降低网络延迟和专线成本

四、关键优化:引入 Filebeat

4.1 为什么需要 Filebeat?

Logstash 的问题

  • 基于 JVM,资源消耗高(CPU、内存)
  • 安装包大(约100MB)
  • 作为 Agent 运行时代价高

Filebeat 的优势

  • 用 Golang 编写,无需 JVM
  • 安装包小(<10MB)
  • 资源消耗极低
  • 专为日志收集优化

4.2 Filebeat 配置示例

1
2
3
4
5
6
7
8
filebeat.prospectors:
- input_type: log
paths: /var/log/nginx/access.log
json.message_key: log

output.elasticsearch:
hosts: ["localhost"]
index: "filebeat-nginx-%{+yyyy.MM.dd}"

4.3 性能对比测试

测试环境

  • 虚拟机:8 cores, 64G内存, 540G SATA盘
  • 数据:350万条日志,单行580B,8进程写入

测试结果

指标 Logstash Filebeat 对比
CPU使用率 53.7% 38.0% Filebeat低30%
处理时间 210秒 30秒 Filebeat快7倍
收集速度 1.6万行/秒 11万行/秒 Filebeat快7倍

五、EFK 完整架构(推荐生产方案)

1
应用服务器 → Filebeat → Kafka集群 → Logstash Indexer → Elasticsearch集群 → Kibana

5.1 各组件职责

  1. Filebeat:轻量级日志收集,部署在每台应用服务器
  2. Kafka:消息队列,缓冲日志数据,保证可靠性
  3. Logstash:集中式日志处理,运行在专用服务器
  4. Elasticsearch:分布式存储和搜索
  5. Kibana:数据可视化和查询界面

5.2 配置要点

Filebeat 到 Kafka

1
2
3
4
output.kafka:
hosts: ["kafka1:9092", "kafka2:9092"]
topic: "app-logs"
required_acks: 1

Kafka 到 Logstash

1
2
3
4
5
6
7
input {
kafka {
bootstrap_servers => "kafka1:9092,kafka2:9092"
topics => ["app-logs"]
codec => json
}
}

六、实践经验与问题解决

6.1 常见问题及解决方案

问题 现象 解决方案
Indexer 挂掉 日志停止消费 使用 Supervisor 监控进程
Java异常换行 异常日志被分割 使用 multiline codec
时区问题 日志时间差8小时 Kibana 使用浏览器时区
Grok解析失败 日志格式不一致 统一日志格式,使用在线调试

6.2 关键配置示例

处理多行日志

1
2
3
4
5
6
7
8
9
input {
stdin {
codec => multiline {
pattern => "^\["
negate => true
what => "previous"
}
}
}

时区处理

1
2
3
4
5
date {
match => [ "log_timestamp", "YYYY-MM-dd HH:mm:ss.SSS" ]
target => "@timestamp"
timezone => "Asia/Shanghai"
}

6.3 监控与维护

  1. 健康检查
    1
    curl -X GET "localhost:9200/_cluster/health?pretty"
  2. 性能监控
    • Elasticsearch:节点状态、索引速率、查询延迟
    • Kafka:堆积量、消费速率
    • Logstash:处理速率、错误计数
  3. 容量规划
    • 根据日志量预估存储需求
    • 设置合理的索引生命周期策略
    • 定期清理过期数据

七、总结与最佳实践

7.1 EFK 架构优势

  1. 高性能:Filebeat 轻量高效,Kafka 缓冲可靠
  2. 可扩展:各组件均可水平扩展
  3. 易维护:职责分离,便于问题定位
  4. 成本可控:根据需求灵活调整资源配置

7.2 部署建议

  1. 开发环境:简单版架构,快速验证
  2. 测试环境:集群版架构,模拟生产
  3. 生产环境:完整 EFK 架构,保证可靠性

7.3 未来演进方向

  1. 容器化部署:使用 Docker 和 Kubernetes 管理
  2. Serverless 架构:利用云服务简化运维
  3. 智能分析:结合机器学习进行异常检测
  4. 安全增强:日志加密、访问控制、审计追踪

7.4 成功关键因素

  1. 标准化:制定统一的日志格式规范
  2. 自动化:部署、配置、监控全流程自动化
  3. 文档化:完善的配置文档和操作手册
  4. 团队培训:确保团队成员掌握相关技能

通过从 ELK 到 EFK 的演进,我们构建了一个高性能、高可靠、易维护的日志系统。这个系统不仅解决了传统日志管理的痛点,还为业务监控、故障排查和安全审计提供了强大支持。随着技术的不断发展,日志系统将继续演进,为企业数字化转型提供更强大的数据支撑。

消息格式

每个提交消息都由一个标题、一个正文和一个页脚组成。而标题又具有特殊格式,包括修改类型、影响范围和内容主题:

1
2
3
4
5
修改类型(影响范围): 标题
<--空行-->
[正文]
<--空行-->
[页脚]

标题是强制性的,但标题的范围是可选的

修改类型

每个类型值都表示了不同的含义,类型值必须是以下的其中一个:

  • feat:提交新功能
  • fix:修复了bug
  • docs:只修改了文档
  • style:调整代码格式,未修改代码逻辑(比如修改空格、格式化、缺少分号等)
  • refactor:代码重构,既没修复bug也没有添加新功能
  • perf:性能优化,提高性能的代码更改
  • test:添加或修改代码测试
  • chore:对构建流程或辅助工具和依赖库(如文档生成等)的更改
代码回滚

代码回滚比较特殊,如果本次提交是为了恢复到之前的某个提交,那提交消息应该以revert:开头,后跟要恢复到的那个提交的标题。然后在消息正文中,应该写上This reverts commit <hash>,其中<hash>是要还原的那个提交的SHA值。

影响范围

范围不是固定值,它可以是你提交代码实际影响到的任何内容。例如$location$browser$compile$rootScopengHrefngClickngView等,唯一需要注意的是它必须足够简短。

当修改影响多个范围时,也可以使用*

标题

标题是对变更的简明描述:

  • 使用祈使句,现在时态:是“change”不是“changed”也不是“changes”
  • 不要大写首字母
  • 结尾不要使用句号
正文

正文是对标题的补充,但它不是必须的。和标题一样,它也要求使用祈使句且现在时态,正文应该包含更详细的信息,如代码修改的动机,与修改前的代码对比等。

页脚

任何Breaking Changes(破坏性变更,不向下兼容)都应该在页脚中进行说明,它经常也用来引用本次提交解决的GitHub Issue

Breaking Changes应该以“BREAKING CHANGE:”开头,然后紧跟一个空格或两个换行符,其他要求与前面一致。

分支功能描述

master: 长期分支,用于对外版本发布,所有版本出自此版本库。此分支不允许直接提交代码,只从bugfix分支和develop分支合并。

develop: 长期分支,用于日常代码开发,与master分支 保持同步,当新功能开发完成后线合并到此分支,经过测试后再合并到master分支。

bugfix: 临时分支,当出现bug时基于master分支新建bugfix/bug-1,bug分支可根据bug编号命名。bug测试完毕合并进入develop分支和master分支

feature: 临时分支,开发新功能时从develop分支新建feature/feature-1,feature分支可根据功能命名。新特性开发完成合并进入develop分支并删除feature分支。

release: 临时分支,需要发布版本时从master分支新建release/release-1.0.0,release分支根据版本号命名。

release分支禁止再合并功能,只提交bug修改,版本发布完成后合并进入master和develop,并再对应的提交上打版本Tag。

提交规范

参考格式

1
2
3
4
5
<type>: <subject>
<BLANK LINE> 空行
<body>
<BLANK LINE> 空行
<footer>
  • type: 本次commit的类型,如bugfix,docs,style等

  • feat: 添加新特性

  • fix: 修复bug

  • docs: 修改文档

  • style: 修改格式缩进,不改变代码逻辑

  • refactor: 代码重构,没有添加新下功能或者修复bug

  • perf: 增加代码进行性能测试

  • test: 增加测试用例

  • chore: 改变构建流程或者增加依赖库、工具等

  • scope: 本次commit波及范围

  • subject: 简明扼要阐述本次commit的主旨

    • 使用祈使句
    • 首字母不要大写
    • 结尾无需添加标点
  • body: 详细描述本次commit,如需换行则使用|

  • footer: 描述下与之关联的 issue 或 breadk change

标题行: 50个字符以内,描述主要变更内容

主体内容: 更详细下说明文本,建议72个字符以内。需要描述信息包括:

  • 为什么这个变更是必须的,它可能是用来修复一个bug,增加一个feature,提升性能、可靠性、稳定性等
  • 如何解决这个问题,具体描述解决问题的步骤
  • 是否存在副作用、风险

如果需要的话可以添加一个连接到issue或其他文档

示例

1
2
3
4
5
6
7
8
9
10
11
12
docs(README): README添加代码提交规范

添加代码规范,提升提交日志的可读性和功能

#123 #没有关联的issue可以省略

----------------------
feat: 增加XXX功能

增加XXX功能,实现XXX效果

#21
0%