作者: 李镇伟

Influxdb oss v2版本-增量数据每日备份与恢复方案设计

1. 方案概述

本方案提供了一套完整的 InfluxDB OSS v2版本数据库异地增量备份与恢复机制,通过在生产环境和恢复环境分别部署 Docker 容器,利用云服务商oss对象存储实现跨地域数据备份和快速故障切换。该方案旨在确保时序数据的安全性、完整性和高可用性,同时优化存储空间利用率。

关联代码: https://github.com/lizhenwei/influxdb_backup_daily

方案目标

  • 建立可靠的数据异地备份机制,防止生产环境数据丢失,
  • 可按日期进行恢复,若出现故障可回滚至最多14天前的数据
  • 实现跨地域的备份恢复流程,支持快速故障切换
  • 管理数据生命周期,平衡存储成本与数据保留需求
  • 提供灵活的配置选项,适应实际业务场景需求

2. 架构设计

本方案采用双环境容器化设计,生产环境部署备份容器,备份地域部署恢复环境运行恢复容器,两者通过云服务商提供的对象存储共享备份数据。
当出现故障时,把备份环境的influxdb数据库目录完整复制到生产环境上,然后决定恢复到那一天


3. 核心功能

3.1 生产环境每日增量备份

备份流程自动执行,每日捕获前一天的增量数据并存储为压缩文件.

3.1.1 关键代码

# -----------------------
# 复制昨天的数据到新 bucket
# -----------------------
influx query \
  --org "$ORG" \
  --host "$HOST" \
  --token "$TOKEN" \
  "from(bucket:\"$SOURCE_BUCKET\")
    |> range(start: ${YESTERDAY}T00:00:00Z, stop: ${YESTERDAY}T23:59:59Z)
    |> to(bucket:\"$BUCKET\")"
# -----------------------
# 备份 bucket
# -----------------------
mkdir -p "$BACKUP_DIR"
influx backup \
  "$BACKUP_DIR" \
  --bucket "$BUCKET" \
  --org "$ORG" \
  --host "$HOST" \
  --token "$TOKEN"

3.1.2 时序图

3.2 备份环境-自动恢复与数据生命周期管理

恢复流程包括数据恢复,和数据生命周期管理.有一个主存储桶mani_bucket和最近14天的归档桶.可根据需求将最近14天的数据回写进主存储桶

3.2.1 关键代码

# -----------------------
# 恢复数据
# -----------------------
influx restore \
    --bucket "$ARCHIVE_BUCKET" \
    --new-bucket "$ARCHIVE_BUCKET" \
    --org "$ORG" \
    --host "$HOST" \
    --token "$TOKEN" \
    "$RESTORE_DIR"
# -----------------------
# 超期数据管理
# -----------------------
# 获取所有归档 bucket 列表(名字以archive_开头的)
  ARCHIVE_BUCKETS=$(influx bucket list --org "$ORG" --host "$HOST" --token "$TOKEN" --json | jq -r '.[] | select(.name | startswith("archive_")) | .name' | sort)
   
  # 计算超过保留天数的日期阈值
  # 使用兼容BusyBox的方式计算:当前时间戳减去保留天数×86400秒
  THRESHOLD_DATE=$(date -d @$(($(date +%s) - ${MAX_RETENTION_DAYS}*86400)) +%s)
   
  # 处理每个归档 bucket
  for BUCKET in $ARCHIVE_BUCKETS; do
    # 从 bucket 名称提取日期
    BUCKET_DATE=$(echo "$BUCKET" | sed 's/^archive_//')
     
    # 将日期转换为时间戳进行比较
    # 注意:BusyBox的date命令支持直接解析YYYY-MM-DD格式
    BUCKET_TIMESTAMP=$(date -d "$BUCKET_DATE" +%s 2>/dev/null)
     
    if [ $? -eq 0 ] && [ $BUCKET_TIMESTAMP -lt $THRESHOLD_DATE ]; then
      echo "处理过期数据: $BUCKET ($BUCKET_DATE)"
       
      # 将数据插回主 bucket
      echo "将 $BUCKET 的数据插回主 bucket $MAIN_BUCKET"
      influx query \
        --org "$ORG" \
        --host "$HOST" \
        --token "$TOKEN" \
        "from(bucket:\"$BUCKET\")
          |> range(start: 0)
          |> to(bucket:\"$MAIN_BUCKET\")"
       
      # 删除过期的 bucket
      echo "删除过期的 bucket: $BUCKET"
      influx bucket delete \
        --name "$BUCKET" \
        --org "$ORG" \
        --host "$HOST" \
        --token "$TOKEN"
    fi 

3.2.2 时序图


4. 技术选型

Alpine Linux3.22容器基础镜像体积小(约5MB)、启动快、资源占用低
InfluxDB CLI2.7.5数据操作工具官方工具,支持完整的数据备份与恢复功能
Docker最新版容器化部署环境隔离、易于部署和管理、跨平台兼容性好
Bash最新版脚本语言轻量级、广泛支持、适合自动化任务
Cron最新版定时任务调度稳定可靠、配置简单、适合周期性任务

5. 环境变量配置

Docker容器的环境变量配置,根据环境灵活调整:

ORG_NAMEmy-orgInfluxDB 组织名称
MAIN_BUCKET_NAMEmy-bucket主数据桶名称
INFLUX_TOKENInfluxDB 访问令牌
INFLUX_HOSThttp://localhost:8086InfluxDB 服务器地址
BACKUP_DIR/backup备份文件存储目录
MAX_RETENTION_DAYS14最多保留archive_bucket天数

6.故障恢复流程

1.切换到备份环境供生产使用
2.使用influxdb backup 把备份环境数据整库复制出来
3.拷贝到生产环境
4.使用influxdb restore –full恢复整库生产环境数据
5.切换数据库到生产环境
6.把切库这段时间缺失的数据从备份环境复制到生产环境


7. 风险与应对

云服务商云对象存储故障备份数据不可用或丢失1.配置云存储多副本机制
跨地域网络延迟或中断备份和恢复操作失败或延迟1. 配置监控,及时发现和解决问题
2. 避开网络使用高峰期
生产环境硬件资源不足备份造成资源不足影响生产使用1.调整备份执行时间,避开使用高峰期
生产环境与恢复环境数据不一致故障切换后数据丢失1. 确保备份和恢复脚本正确执行
2. 定期验证备份数据的完整性和可恢复性
3. 配置数据同步监控,及时发现数据不一致问题
InfluxDB 服务不可用备份和恢复操作失败1. 监控 InfluxDB 服务状态,设置告警机制
2. 定期进行服务健康检查
访问令牌泄露数据安全风险1. 遵循最小权限原则,为备份和恢复操作分配适当权限
2. 定期更换访问令牌
3. 使用密钥管理服务存储敏感凭证
4. 不在不安全的地方存储令牌
Docker 容器故障备份和恢复计划中断1. 监控容器运行状态,设置自动重启策略
2. 配置容器健康检查
3. 定期更新容器镜像和基础系统
只备份前一天的数据,不备份当前数据遇到故障时,当天的数据无法恢复1.只能从sdlink的timescaledb数据里想办法,写代码恢复到influxdb里了
2.或者使用influx remote 和influx replication做双写,但是这样就需要再有一个inflxudb实例.需要增加云服务器成本

使用阿里云服务器部署traefik,关联NLB负载均衡器,添加cert-manager免费证书认证

背景需求

我们有一个常规的网站服务需要部署,并且对外提供https访问,从经济的角度考虑,建议购买阿里云的【容器集群ACK【网站域名】【NBL负载均衡】【共享带宽包】【云服务器ECS】,如果有文件需求还可以购买【对象存储】【NAS文件系统】等,有静态文件加速需求还可以购买CDN服务,本篇文章我使用最低需求(钱),仅购买3台服务器,组成k8s集群,部署web网站,自动使用acme.sh申请证书,使用外部负载均衡器来打造一个最低限度的高可用生产环境。

忽略的细节

从阿里云官网购买【容器集群ACK【网站域名】【NBL负载均衡】【共享带宽包】【云服务器ECS】本文忽略,默认读者已购买并添加好,并复制kubeconfig文件到服务器上,kubectl和helm程序已经安装好,接下来直接敲命令

1.安装cert-manager

helm repo add jetstack https://charts.jetstack.io
 
helm repo update
 
helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.15.3 \
  --set installCRDs=true

2.配置cert-manager所需要的阿里云dns挑战

安装阿里云dns挑战所对应的webhook

helm repo add cert-manager-alidns-webhook https://devmachine-fr.github.io/cert-manager-alidns-webhook
helm repo update
helm install alidns-webhook cert-manager-alidns-webhook/alidns-webhook

添加alidns-secret.yaml 文件,注意这里的access-key和secret-key是要通过阿里云的accesskey功能去获取的,获取之后,通过echo命令获取base64加密后的文本,填入yaml文件中

echo -n "原始密钥" | base64 

# alidns-secret.yaml 文件
apiVersion: v1
kind: Secret
metadata:
  name: alidns-secret
  namespace: cert-manager
data:
  access-key: base64加密后的
  secret-key: base64加密后的
kubectl apply -f alidns-secret.yaml

添加letsencrypt-staging.yaml文件,注意不要修改groupName,因为我上面helm install alidns-webhook的时候使用的默认参数里的groupName是example.com,一定要修改groupName的话,需要两边同步修改

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: 357244849@qq.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
    - dns01:
        webhook:
          groupName: example.com
          solverName: alidns-solver
          config:
            region: ""
            accessKeySecretRef:
              name: alidns-secret
              key: access-key
            secretKeySecretRef:
              name: alidns-secret
              key: secret-key
kubectl apply -f letsencrypt-staging.yaml

添加Certificate.yaml文件,这一步完成后,手动去阿里云的域名解析里添加对应的cname解析了,记录值填负载均衡器给到的域名,一般是nlb-xxxx.地域.nlb.aliyuncs.com

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: hello-com
  namespace: traefik
spec:
  # The secretName will store certificate content
  secretName: hello-com-tls
  dnsNames:

  - "*.hello.com"
  - "ops.hello.com"
  issuerRef:
    name: letsencrypt-staging
    kind: ClusterIssuer
kubectl apply -f Certificate.yaml

3.安装traefik并关联对应的证书和负载均衡器

1.编辑一个values-traefik.yaml文件,可以参考我下面的配置,

providers:
  kubernetesCRD:
    allowCrossNamespace: true
  kubernetesIngress:
    publishedService:
      enabled: true # 让 Ingress 的外部 IP 地址状态显示为 Traefik 的 LB IP 地址
service:
  enabled: true
  loadBalancerClass: alibabacloud.com/nlb
  annotations:
    service.beta.kubernetes.io/alibaba-cloud-loadbalancer-id: "NLB的ID" # 关联阿里云NLB负载均衡器的ID。
    service.beta.kubernetes.io/alibaba-cloud-loadbalancer-force-override-listeners: "true"
  spec:
    externalTrafficPolicy: Local
# 这里不加的话,80和 443 会报没有权限
securityContext:
  capabilities:
    add:
      - NET_BIND_SERVICE
  runAsNonRoot: false
  runAsUser: 0
updateStrategy:
  # -- Customize updateStrategy: RollingUpdate or OnDelete
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 1
    maxSurge: 0
ports:
  web:
    port: 80
    expose:
      default: true
    exposedPort: 80 # 对外的 HTTP 端口号,使用标准端口号在国内需备案
    redirectTo:
      port: websecure
  websecure:
    port: 443
    expose:
      default: true
    exposedPort: 443 # 对外的 HTTPS 端口号,使用标准端口号在国内需备案
logs:
  access:
     enabled: true
deployment:
  enabled: true
  replicas: 3
ingressRoute:
  dashboard:
    enabled: true
    matchRule: Host(`traefik.hello.com`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))
    entryPoints: ["websecure"]
    middlewares:
      - name: traefik-dashboard-auth
extraObjects:
  - apiVersion: v1
    kind: Secret
    metadata:
      name: traefik-dashboard-auth-secret
    type: kubernetes.io/basic-auth
    stringData:
      username: hello
      password: thankyou

  - apiVersion: traefik.io/v1alpha1
    kind: Middleware
    metadata:
      name: traefik-dashboard-auth
    spec:
      basicAuth:
        secret: traefik-dashboard-auth-secret
# 关联cert-manager设置的秘钥
tlsStore:
  default:
    defaultCertificate:
      secretName: hello-com-tls
helm repo add traefik https://helm.traefik.io/traefik
helm repo update
helm upgrade --install traefik -n traefik -f values-traefik.yaml traefik/traefik

安装完成后,使用kubectl get svc -n traefik就能看到生成的loadbalancer了,通过阿里云控制台也可以看到网络型负载均衡器里面自动创建了对应的监听和服务器组,如需验证部署后的效果,可以用浏览器访问https://traefik.hello.com/dashboard 进行测试

python实现的一个简易注册机,用于离线工控机验证注册码

简易的设计流程

有一批工控机长期是断网离线的,而我们又想检查这批工控机的设备上的注册码是否已经过期,那该怎么办呢?工控机是交付出去了,需要用到非对称加密机制来设计,防止算法被破解之后,别人有能力能破解我们所有的工控机。下面废话不多说,直接上代码

服务端程序

1. 安装 cryptography

pip install cryptography

2. 生成密钥对(一次性操作)

在服务器端生成 RSA 公钥和私钥,这个过程只需要进行一次,并且私钥需要保密。

# 注册机密钥对生成代码
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# 生成 RSA 密钥对
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
)

# 将私钥保存到文件
with open("private_key.pem", "wb") as private_file:
    private_file.write(
        private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.NoEncryption(),
        )
    )

# 将公钥保存到文件
public_key = private_key.public_key()
with open("public_key.pem", "wb") as public_file:
    public_file.write(
        public_key.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo,
        )
    )

private_key.pem 文件将用于服务器端签名注册码。(千万不要泄漏)

public_key.pem 文件可以安全地分发给客户端,用于验证签名。

3. 服务器端生成注册码并签名

服务器端生成注册码并用私钥签名。这里我们基于 IP、MAC 地址和注册时间生成注册信息。

# 注册码生成程序
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
import hashlib

# 加载私钥
with open("private_key.pem", "rb") as private_file:
    private_key = serialization.load_pem_private_key(
        private_file.read(),
        password=None,
    )


# 生成注册信息
def generate_registration_info(ip, mac, reg_time):
    return f"{ip}#{mac}#{reg_time}"


# 生成签名
def sign_registration_info(private_key, registration_info):
    # 先对注册信息进行哈希处理
    hash_value = hashlib.sha256(registration_info.encode()).digest()
    # 使用私钥对哈希值签名
    signature = private_key.sign(
        hash_value,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH,
        ),
        hashes.SHA256(),
    )
    return signature


# 示例使用
ip = "192.168.1.1"
mac = "00:1A:2B:3C:4D:5E"
reg_time = "2023-09-03T12:00:00"
registration_info = generate_registration_info(ip, mac, reg_time)
signature = sign_registration_info(private_key, registration_info)

print(f"注册信息: {registration_info}")
print(f"签名: {signature.hex()}")

客户端程序(可参考修改)

1.安装在工控机上,用于验证注册码是否正确,是否过期,设置有效期365天

2.客户端使用公钥来验证签名,以确保注册码的有效性。

# 注册机尝试
import hashlib
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
from datetime import datetime

# 加载公钥
with open("public_key.pem", "rb") as public_file:
    public_key = serialization.load_pem_public_key(public_file.read())


# 验证签名
def verify_registration_info(public_key, registration_info, signature):
    # 对注册信息进行哈希处理
    hash_value = hashlib.sha256(registration_info.encode()).digest()
    # 使用公钥验证签名
    try:
        public_key.verify(
            signature,
            hash_value,
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.MAX_LENGTH,
            ),
            hashes.SHA256(),
        )
        return True
    except Exception as e:
        print(f"签名验证失败: {e}")
        return False


# 检查注册码是否过期
def is_registration_expired(registration_info):
    # 假设 registration_info 的格式为 "IP#MAC#DATE"
    try:
        _, _, date_str = registration_info.split("#")
        registration_date = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S")
        current_date = datetime.now()
        # 计算日期差
        difference = current_date - registration_date
        if difference.days > 365:
            print("注册码已过期。")
            return True
        else:
            print(f"注册码有效,距离到期还有 {365 - difference.days} 天。")
            return False
    except Exception as e:
        print(f"无法解析注册信息中的日期: {e}")
        return True


# 示例使用
registration_info = "192.168.1.1#00:1A:2B:3C:4D:5E#2023-09-03T12:00:00"
signature = "4747554cb3bc12f1b25f7079f435338f78f8b9a096f7a40fe6f4d22535d5d1038ae0cff10f1f7d648201e585b19b15d45a0b0903886abda9096f7ce3dbba78b3076f7df9cec9f19616512c7dd20a4448ab1eb544be0163e84cb811cf415986551f4be21878c4ca620843e4109d4e625756560f0746dbbccd2f57ee1ba6d8f751bde45839e75229160371c955bc8d19931d647d1281f4ae6baa08dd460dbf3de0d1ac76f2217ed8e81657cce00da6342ef5d453afb0c24da10197896f89347a3dc81a482eab2e41ffe311222e86d0e6a0901ae6cce38e69700d7d4a18c9e902ea802b05292c166d561b4877619283026542e319ca35708fa32e6f86e5615f6fb0"
is_valid = verify_registration_info(
    public_key, registration_info, bytes.fromhex(signature)
)

if is_valid:
    print("注册码验证成功。")
    # 检查注册码是否过期
    is_expired = is_registration_expired(registration_info)
    if not is_expired:
        print("激活成功!")
else:
    print("注册码验证失败。")

UPS软件关闭多台linux服务器的方法

背景

因为我们的施工现场往往是有一到多台服务器的,这些服务器有的是双路电源,有的是只有UPS供电。众所周知,服务器如果遇到突然断电,是有损坏硬盘的风险的,为了避免断电停机的风险,一般会设置UPS来给服务器供电,如果需求支撑长时间供电,甚至会加上储能设备,但是只要不来电,存储的电终究是有用完的时候的,所以我们还额外需要在其中一台服务器上安装一个agent程序。用于和UPS进行通信,实时检查UPS的剩余电量,如果剩余电量不足时,通过ssh关闭机房里的所有服务器

示意图

1.UPS的控制程序winpower下载

接下来的文章,我们将使用山特的linux版本的winpower程序来做配置

下载地址https://www.santak.com.cn/page/santak-downloads.html

我们可以在软件列表找到linux版本的winpower,如果第一页没有就在第二页

2.在linux服务器上安装winpower

在服务器上解压下载下载好的压缩文件Winpower_setup_LinuxAMD64.tar.gz

解压之后进入到该目录下的LinuxAMD64下,里面有一个install.bin文件,这里别急着安装,得先安装几个包,不然会报错

sudo apt install -y libxtst6 libxi6 x11-common libxrender1

安装完后再输入winpower的安装命令,过程中遇到提示一路按回车就可以了

./install.bin -i console

安装完成后,winpower程序会安装在/opt/MonitorSoftware/目录下,建议此时可以重启一次电脑,这样会自动启动agent程序,如果不重启的话,可以手动开启agent,命令如下

sudo systemctl start upsagent.service

3.配置winpower

linux版本的必须进入/opt/MonitorSoftware/目录下然后输入命令./monitor进行启动,注意如果是ssh到服务器上的话,需要用一个支持x11转发的ssh工具,像putty或者xshell,powershell肯定是不行了,可以去下载一个mobaxterm来用。

启动之后,先退出向导界面

然后成为系统管理员,默认密码是空

接下来是打开通讯口设定

我们是需要UPS上接一个RS232转USB的线到服务器上的,要检查linux服务器上的USB的串口信息可以用如下命令,把查出来的USB信息填到winpower的通讯口信息里

ls -l /dev/ttyUSB*

添加完成后,再从菜单栏里,点击自动搜索设备,一般1-2分钟就能搜索到UPS了

接下来是配置关机参数设定

建议配置

1.【允许放电时间】建议勾选。因为实际现场使用来看,这个串口通信并不总是很稳定,如果不勾选,可能agent一直获取不到电池百分比信息,默认是会在市电断开2小时后才关机,而30分钟之后可能UPS都没电了,这不是没用么。所以这个功能和【当电池容量百分比低于80%】可以相辅相成,那个先满足,那个先触发。

2.【低电位立即关机】,【剩余放电时间少于10分】,可选可不选,因为实际现场应该用不到

3.【系统】单选【关闭】。这个必须这样选,因为这个是和下面的【关机前执行档案】是联动的,如果选休眠就没用

4.【关机前执行档案】。我的建议是使用python去写一个程序,然后用pyinstaller打包出来放到这里执行。如果一定想用shell脚本也是可以的,这里如果存在程序调用程序或者配置文件,那需要非常注意路径,因为这个程序是会在/opt/MonitorSoftware/目录下执行。建议如果存在程序调用配置文件,配置文件的路径使用绝对路径。例如我的ups_shutdown程序是使用了paramiko库和os库去查找配置文件、读取需要远程关闭的服务器信息、ssh成功之后按需求去安全关闭服务器上的应用程序,数据库等再执行shutdown的关机命令。

debian12虚拟机导出后再导入无法开机的解决方法

遇到的问题

在迁移debian12虚拟机时,我从esxi导出vmdk和ovf文件,再到另外一台esxi上导入时,出现了找不到硬盘,无法开机的问题,报错:

EFl Virtual disk (0.0)… No compatible bootloader found
EFI VMware Virtual SATA CDROM Drive (0.0)...Mo Media.
EFI Network..

解决方法

1.编辑虚拟机选项,勾选强制执行bios设置

2.重新开机进入bios,选择enter setup

3.选择boot from a file

4.找到硬盘,选择硬盘

5.选择EFI,按回车

6.选择debian

7.选择grubx64.efi,按回车

8.系统跳到grub界面,选择debian进入系统

后续操作,重新设置grub

因为重新导入之后,这个grub信息丢失了,所以我们需要找到efi所在硬盘,并且重新设置grub

# 更新grub设置
update-grub
# 通过df命令找到分区
df -h
# 重新安装grub
grub-install /dev/sda1

docker运行的postgresql优雅关闭

背景

我们的许多项目部署完之后,是会经常关闭或者重启服务器的,比如各种过年过节的日子,停工的日子,这一关机,数据库就容易出问题。我们的postgresql是运行在我自定义的一个debian容器中,因为添加了一些我自定义的定时备份和日志功能,所以运行方式和官方镜像不一致。为了减少数据库因为关机而造成出问题的情况,得想一个优雅关闭的方法(至少不能原地爆炸^_^)

第一个进行不下去的方案(systemctl)

在systemctl里是可以写关机前运行的服务的,但是我遇到了一个问题,我编写的这个服务是在输入关机命令后,先关闭了docker服务才启动这个服务,这会导致数据库关闭命令的时候,因为docker提前关闭而失败,这个方案告终。

命令执行效果是:shutdown–>docker.socket关闭–>pg_shutdown.service关闭

[Unit]
Description=pg_shutdown
DefaultDependencies=no
Before=shutdown.target

[Service]
Type=oneshot

ExecStart=/usr/bin/docker exec -it pg_ctlcluster 14 main stop --m fast

[Install]
WantedBy=shutdown.target

最终采用的方案

既然我们需要在docker关闭时执行命令,那么就得调头去找docker是如何处理接受“终止”信号的方案。根据docker官网的知识,我们可以得知,当触发docker stop或者关机的时候,是会向容器发送一个SIGTERM信号的,我们需要在dockerfile里,修改启动脚本,添加一个“安全关闭”的函数,并且让脚本通过钩子把SIGTERM信号与“安全关闭”函数连在一起。当收到SIGTERM信号时就“安全关闭”

#Dockerfile文件参考


FROM debian:12
# 此处省略安装postgresql过程
....
# 然后放一个docker-entrypoint.sh文件进去
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

#docker-entrypoint.sh文件参考


#!/usr/bin/env bash
file_path="/var/run/postgresql/.s.PGSQL.5432.lock"
if [ -f "$file_path" ]; then
    rm "$file_path"
fi
service cron start
pg_ctlcluster 14 main start

stop_container() {
  mkdir -p /var/lib/postgresql/14/main/log/
  echo "$(date) - Stopping PostgreSQL..." >> /var/lib/postgresql/14/main/log/stop.log
  pg_ctlcluster 14 main stop --m fast
  echo "$(date) - PostgreSQL stopped." >> /var/lib/postgresql/14/main/log/stop.log
}
trap 'stop_container' SIGTERM
tail -f /var/log/postgresql/*.log &
wait

这边有一点要注意的是,结尾需要加& wait 而不能只用tail ,不然stop_container不会触发的

但是到这里还没有完全结束,因为postgresql的关闭有时候不那么快,默认的docker 关闭的超时时间是10秒,为了求稳,我们还需要修改docker运行容器时的超时时间,在docker run的时候增加参数–stop-timeout=60,注意,这个超时时间只能在docker run容器的时候去设置,通过修改/etc/docker/daemon.json里的shutdown-timeout是无效的

最后

经过一番修改与重新编写程序,我又测试了多次poweroff和reboot检查,确定稳如老狗之后,终于将其发布至现场,以后可以安心过节啦。虽然看起来修改的代码不多,但是要知道在那修改,如何修改有效,总共花了我一天的时间($_$)

在Kubernetes里使用Traefik插件实现IP黑名单功能

背景

网上关于traefik的ip黑名单功能的文章几乎没有,ChatGPT讲解的也不太对,于是我根据自己的使用经验记录下在kubernetes里,使用traefik的denyip插件,配置IP黑名单功能,可以对单独的ingress(域名)生效,也可以对整个entrypoint(端口)生效


traefik的denyip安装

1.去traefik插件中间,找到denyip插件的信息

https://plugins.traefik.io/plugins/62947363ffc0cd18356a97d1/deny-ip-plugin

2.参考traefik官方helmchart编写values-traefik.yaml文件(https://github.com/traefik/traefik-helm-chart/blob/master/traefik/values.yaml)

# 这里只贴下载插件模块的代码
experimental:
  plugins:
    denyip:
      moduleName: github.com/kevtainer/denyip
      version: v1.0.0

3.安装traefik

helm upgrade --install traefik -n traefik -f values-traefik.yaml traefik/traefik --version 26.0.0
#安装完之后,可以检查pod,会发现在pod配置里添加上了插件信息
kubectl describe pod -n traefik traefik-txxqm

对整个entrypoint限制IP访问(例如限制IP访问80端口下的所有域名)

1.添加middleware.yaml文件,例如禁用192.168.1.1的IP访问

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
    name: denyip
    namespace: traefik
spec:
    plugin:
        denyip:
            ipDenyList:
                - 192.168.1.1

添加完后,使用kubectl apply -f middleware.yaml命令使其生效

2.编辑values-traefik.yaml文件,修改ports部分,找到80端口部分,修改代码如下,只生效于80端口,443端口不生效。

ports:
  web:
    port: 80
    expose: true
    exposedPort: 80 # 对外的 HTTP 端口号,使用标准端口号在国内需备案
    middlewares:
     - traefik-denyip@kubernetescrd
  websecure:
    port: 443
    expose: true
    exposedPort: 443 # 对外的 HTTPS 端口号,使用标准端口号在国内需备案

3.再次安装traeifk

helm upgrade –install traefik -n traefik -f values-traefik.yaml traefik/traefik –version 26.0.0

4,去dashboard页面检查,会发现在这个entrypoint的所有ingress的配置里,都会加上该middleware


对单一IngressRoute生效(例如对指定域名或路径)

1.添加middleware.yaml文件,例如禁用192.168.1.1的IP访问

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
    name: denyip
    namespace: traefik
spec:
    plugin:
        denyip:
            ipDenyList:
                - 192.168.1.1

添加完后,使用kubectl apply -f middleware.yaml命令使其生效

2.这里有一个注意点,往往我们的ingress和middleware不一定在同一个namespace,这个时候需要在安装traefik的时候启用【允许使用跨命名空间】功能,修改values-traefik.yaml,添加如下配置:

providers:
  kubernetesCRD:
    allowCrossNamespace: true

然后再次安装

helm upgrade --install traefik -n traefik -f values-traefik.yaml traefik/traefik --version 26.0.0

3.修改需要使用middleware的ingressroute文件,和service同级增加middlewares信息

4.修改完成后,再去dashboard页面检查,会发现只有该http route有middleware信息,不会影响其他域名的正常访问

迁移drone.io的sqlite数据库到postgresql数据库

背景

drone.io默认安装的时候是使用sqlite数据库,可以用于演示产品,但是一旦git仓库数量过多,build次数过多之后,会越用越卡,从长远来看,想用于生产环境,还是得转向postgresql数据库。关于postgresql数据库的配置可以参考如下页面。主要是设置一个数据库来源和postgresql数据库连接串信息

https://docs.drone.io/server/storage/database/#postgres

实施步骤

1.找到drone.io的原始sqlite数据库。

如果我们采用的是kubernetes的helmchart安装,一般会在drone命名空间里的pvc里找到,可以通过 kubectl命令去找

kubectl get pvc -n drone
kubectl get pv 
kubectl describe pv $(drone的pv名字)

找到存储位置后,具体的sqlite数据库文件叫database.sqlite 可以通过CP命令复制一份database.sqlite出来备份

2.安装postgresql数据库

postgresql数据库的安装还是比较简单的,安装完成之后,再创建一个用于存储drone数据的数据库实例和用户

sudo apt update
sudo apt install postgresql
su postgres
psql
create user 数据库用户名 with password '数据库密码';
create database 数据库实例 owner 数据库用户名;
revoke all on database 数据库实例 from public;

3.安装pgloader工具

pgloader是一款用于从其他数据库导数据到postgresql的工具,此工具安装和使用比较方便,但是有一个缺陷,占用内存会比较大,比如我这里要导入的sqlite数据库有5个G,那么该程序在运行的过程中至少要用10个G的内存,如果不够的话,会在运行一段时间之后被oom kill 掉,加上系统本身运行需要一些内存,我建议得准备一个16G内存的服务器

apt-get install pgloader
#安装完成后添加一个db.load文件,内容如下:
load database
     from sqlite:///home/sfere/database.sqlite
     into postgresql://数据库用户名:数据库密码@数据库ip/数据库实例

     with truncate,
          create tables,
          create indexes,
          reset sequences
;

4.转换前需要重建sqlite数据库”构建信息表

如果不做这一步,会报错

ERROR Database error 42704: type “number” does not exist 

所以我们重建builds表

CREATE TABLE builds1
(
  build_id            bigserial,
  build_repo_id       bigint,
  build_trigger       text,
  build_number        bigint,
  build_parent        bigint,
  build_status        text,
  build_error         text,
  build_event         text,
  build_action        text,
  build_link          text,
  build_timestamp     bigint,
  build_title         text,
  build_message       text,
  build_before        text,
  build_after         text,
  build_ref           text,
  build_source_repo   text,
  build_source        text,
  build_target        text,
  build_author        text,
  build_author_name   text,
  build_author_email  text,
  build_author_avatar text,
  build_sender        text,
  build_deploy        text,
  build_params        text,
  build_started       bigint,
  build_finished      bigint,
  build_created       bigint,
  build_updated       bigint,
  build_version       bigint,
  build_debug         bool,
  build_cron          text default '',
  build_deploy_id     bigint default '0'
);

INSERT INTO builds1 SELECT * FROM builds;

drop table builds;

ALTER TABLE `builds1` RENAME TO `builds`

5.开始导入数据到postgresql,导入完成后检查postgresql数据库里是否有对应的数据

pgloader db.load 

6.导入完成,更新drone server,可以通过修改kubernetes资源的方式,或者修改helm chart 的values.yaml的方式添加环境变量

DRONE_DATABASE_DRIVER=postgres DRONE_DATABASE_DATASOURCE=postgres://用户名:密码@数据库IP:5432/数据库实例?sslmode=disable

把electron程序作为服务部署到debian11系统上

背景

我们的操作系统是debian11,桌面系统gnome,使用electronjs开发了一个linux桌面端程序,需要部署上去,加入系统服务,并设置成开机自启

技术要点

  1. gnome桌面可以使用wayland和xorg登录,我们这里采用的是xorg方式
  2. electronjs使用electron-builder可以打包一个deb包文件,但是deb包里是没有service文件的,这个service文件我们需要自己写
  3. deb包的安装需要以root用户进行,而系统服务中运行xorg不能以root用户进行,而是以登录gnome的用户进行,这里面需要在定制deb包的时候,进行反复套娃

步骤简述

1.先使用electron打包命令 yarn electron-builder –linux 打包出deb需要包含的文件目录

2.编写一个service文件和postinst,postrm,prerm脚本(重要,talk is cheap ,show me the code)

# service文件的内存参考,注意设置环境变量以适配xorg
[Unit]
Description=electron app
After=network.target

[Service]
Type=simple
WorkingDirectory=/工作路径
ExecStart=/工作路径/electron二进制程序
SuccessExitStatus=143
TimeoutStopSec=10
Environment="DISPLAY=:0" "XAUTHORITY=/home/sfere/.Xauthority"
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
#postinst文件参考,因为deb包是用root用户安装的,这里注意要切换到普通用户上去设置开机自启
#!/bin/sh
set -e
export XDG_RUNTIME_DIR=/run/user/1000
su lizhenwei -c 'systemctl --user daemon-reload'
su lizhenwei -c 'systemctl --user enable dscs.service'
su lizhenwei -c 'systemctl --user start dscs.service'
exit 0
# postrm文件参考,如果要删除electron程序,注意要重新加载一次用户服务,避免后台依然再查询改electron服务,会在journalctl 里看到大量的服务找不到的报错
#!/bin/sh
set -e
export XDG_RUNTIME_DIR=/run/user/1000
su lizhenwei -c 'systemctl --user daemon-reload'
exit 0
#prerm文件参考,如果直接执行 apt purge命令卸载electron程序,是不会立刻杀死在gnome前端运行的electron程序的,这里就要通过pkill的方式来杀死一次electron程序
#!/bin/sh
set -e
pkill -f /工作路径/electron二进制程序
exit 0

3.service文件的位置,在deb里的路径需要配置好,放在/home/用户名/.config/systemd/user/下

常见问题

1.如果把electron服务装在了/lib/system/systemd目录下,就会变成root用户启动,这种情况下,electron应用程序也能在桌面打开,但是会报错,报错内容如下:

ERROR:bus.cc(399)] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix")

解决的办法就是把服务放在/home/用户名/.config/systemd/user/下运行

2.apt purge electron程序 之后,gnome桌面上程序没有关闭,需要手动pkill杀进程,或者参照我上面的代码改写deb包的prerm文件,这个文件也可以在这个路径找到:/var/lib/dpkg/info/[electron程序包名].prerm

生成oracle客户端docker镜像的两种玩法

背景

我们的oracle服务端是oracle12g版本,应用程序均由golang或者python编写,运行在k8s 容器里,那我们就需要build一些docker容器来,那如何build呢?可以参考以下我的方法。文章最后有我编写过程中的参考文章,也可以根据参考文章自己创新。

玩法1:参考oracle官方文档制作

1.下载代码https://github.com/oracle/docker-images.git 到本地

2.进入OracleInstantClient/oraclelinux8/21/目录,该目录下有一个原始的dockerfile文件,可以使用该文件build一个基础镜像,例如

docker build --pull -t oracle/instantclient:21 .

使用build出来的这个oracle/instantclient:21镜像可以二次进行dockerfile编辑加入golang或者python。

也可以用这个oracle/instantclient:21来测试一下oracle数据库是否能正常连接。测试连接命令如下:

docker run -ti --rm oracle/instantclient:21 sqlplus 用户名/密码@数据库IP:数据库端口/数据库名

玩法2:从debian开始制作一个镜像

除了上面的方法外,我们还可以从debian开始制作一个包含python的镜像

1.进入oracle客户端下载页https://www.oracle.com/database/technologies/instant-client/linux-x86-64-downloads.html

2.下载https://download.oracle.com/otn_software/linux/instantclient/218000/instantclient-basic-linux.x64-21.8.0.0.0dbru.zip到本地,如下图

3.编写Dockerfile,以debian+oracle+python举例

FROM debian:11-slim

LABEL maintainer="zhenwei.li <zhenwei.li@sfere-elec.com>"
RUN set -eux \
    && sed -i "s@http://ftp.debian.org@https://repo.huaweicloud.com@g" /etc/apt/sources.list \
    && sed -i "s@http://security.debian.org@https://repo.huaweicloud.com@g" /etc/apt/sources.list \
    && apt-get update \
    && apt-get install -y -q libaio1 unzip python3 pip
    && pip install cx_Oracle

# 清理垃圾
RUN set -eux \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    && rm -rf /tmp/*
ENV TZ=Asia/Shanghai \
    DEBIAN_FRONTEND=noninteractive

RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \
    && echo ${TZ} > /etc/timezone \
    && dpkg-reconfigure --frontend noninteractive tzdata \
    && rm -rf /var/lib/apt/lists/*

COPY instantclient-basic-linux.x64-21.8.0.0.0dbru.zip /opt/oracle/instantclient-basic-linux.x64-21.8.0.0.0dbru.zip

WORKDIR /opt/oracle/

RUN unzip instantclient-basic-linux.x64-21.8.0.0.0dbru.zip

RUN sh -c "echo /opt/oracle/instantclient_21_8 > /etc/ld.so.conf.d/oracle-instantclient.conf"

RUN ldconfig

RUN useradd sfere

4. 目录下放Dockerfile和oracle客户端zip包

5. 制作镜像

docker build -t debian-oracle .

6.运行镜像,测试python连接oracle服务端可行,依次输入如下命令

docker run -ti --rm debian-oracle python
import cx_Oracle as cx
con = cx.connect('用户名', '密码', '数据库IP:数据库端口/数据库名')

参考文章

https://github.com/oracle/docker-images/tree/main/OracleInstantClient

https://csiandal.medium.com/install-oracle-instant-client-on-ubuntu-4ffc8fdfda08


苏ICP备18047533号-1