Posted in

Go批量写入MySQL必踩的8个坑(附可直接落地的go-sql-driver优化配置清单)

第一章:Go批量写入MySQL的底层原理与性能瓶颈全景图

Go 应用向 MySQL 批量写入数据时,表面是调用 db.Exec 或 stmt.Exec,实则横跨应用层、驱动层、协议层、服务端执行引擎四重边界。底层依赖 database/sql 的连接池管理与 mysql 驱动(如 go-sql-driver/mysql)实现二进制协议通信,每条 INSERT 语句经序列化为 COM_QUERY 包,由 TCP 流分帧传输;而真正的批量效率提升,必须绕过单语句逐条提交的路径,转向多值插入(INSERT INTO t(a,b) VALUES (?,?),(?,?))、预处理语句复用或 LOAD DATA LOCAL INFILE 协议级优化。

数据包与网络往返开销

单条 INSERT 触发至少一次 RTT(请求+响应),1000 行数据若拆成 1000 次调用,网络延迟将主导耗时。使用多值语法可将 1000 行压缩为 1 条语句(需控制单语句长度 ≤ max_allowed_packet,默认 64MB):

// 构建动态多值 SQL(注意参数数量限制)
values := make([]interface{}, 0, len(rows)*2)
placeholders := make([]string, 0, len(rows))
for _, r := range rows {
    placeholders = append(placeholders, "(?,?)")
    values = append(values, r.A, r.B)
}
sql := "INSERT INTO users(name,age) VALUES " + strings.Join(placeholders, ",")
_, err := db.Exec(sql, values...) // 驱动自动绑定并序列化为紧凑二进制包

连接池与事务一致性

默认连接池(SetMaxOpenConns(0) 即不限制)在高并发下易引发连接争用。批量写入应显式复用连接并包裹事务:

参数 推荐值 说明
SetMaxOpenConns 20–50 避免 MySQL 端线程创建风暴
SetMaxIdleConns 同上 减少空闲连接维持开销
SetConnMaxLifetime 30m 防止长连接因网络中断僵死

存储引擎层瓶颈

InnoDB 在批量写入时面临:

  • 聚簇索引页分裂(主键非递增时)
  • 二级索引维护的随机 I/O 放大
  • Redo Log 刷盘频率(innodb_flush_log_at_trx_commit=1 下每事务强制刷盘)

启用 innodb_buffer_pool_size ≥ 70% 物理内存,并在事务内控制 INSERT 批次大小(建议 1k–10k 行/事务),可平衡 WAL 压力与崩溃恢复粒度。

第二章:连接管理与复用的致命误区

2.1 连接池配置不当导致连接耗尽与TIME_WAIT风暴

当连接池最大连接数(maxActive)设为 20,而并发请求峰值达 50 时,线程持续阻塞等待连接,触发超时重试——大量短连接被创建并快速关闭。

常见错误配置示例

// 错误:未启用连接空闲回收,minIdle=0,maxWaitMillis=3000
BasicDataSource ds = new BasicDataSource();
ds.setMaxActive(20);        // 连接上限过低
ds.setMinIdle(0);           // 空闲连接不保活,冷启动压力大
ds.setTestOnBorrow(true);   // 每次借连接都执行 validationQuery,开销陡增

逻辑分析:testOnBorrow=true 在高并发下使每次获取连接增加一次数据库 round-trip;minIdle=0 导致流量突增时需频繁新建连接,加剧 TIME_WAIT 积压。

TIME_WAIT 风暴成因

参数 推荐值 影响
net.ipv4.tcp_fin_timeout 30s 缩短 FIN_WAIT_2 → TIME_WAIT 转换周期
net.ipv4.ip_local_port_range 1024 65535 扩大可用端口池,缓解端口耗尽
graph TD
    A[应用发起HTTP请求] --> B[从连接池获取DB连接]
    B --> C{连接可用?}
    C -->|否| D[阻塞等待或创建新连接]
    C -->|是| E[执行SQL]
    E --> F[连接归还池]
    D --> G[短连接频繁创建/关闭]
    G --> H[内核堆积大量TIME_WAIT socket]

根本症结在于连接复用率低 + 回收策略缺失,使连接生命周期趋近于“用完即关”。

2.2 短生命周期连接在高并发批量场景下的RT飙升实测分析

在压测某金融对账服务时,当QPS升至1200+、每请求创建/关闭MySQL连接(maxLifetime=30s),P99响应时间从87ms骤增至1420ms。

连接风暴现象

  • 每秒新建1300+ TCP连接,TIME_WAIT堆积超8000;
  • 内核net.ipv4.tcp_tw_reuse=0导致端口复用失效;
  • 应用层未启用连接池,DriverManager.getConnection()成为性能瓶颈。

关键复现代码

// ❌ 反模式:每次查询新建连接
for (int i = 0; i < 100; i++) {
    try (Connection conn = DriverManager.getConnection(url, user, pwd); // 耗时≈15–35ms/次
         PreparedStatement ps = conn.prepareStatement("SELECT * FROM tx WHERE id=?")) {
        ps.setLong(1, ids[i]);
        ps.executeQuery(); // 实际业务逻辑
    }
}

该代码在JMeter 200线程下触发连接建立阻塞,getConnection()平均耗时跃升至28ms(含DNS解析+TCP三次握手+SSL协商),直接拉高端到端RT。

优化对比(1000 QPS下)

方案 P99 RT 连接复用率 TIME_WAIT峰值
原始短连 1420ms 0% 8246
HikariCP池化 92ms 99.8% 17
graph TD
    A[客户端发起请求] --> B{连接池有空闲连接?}
    B -->|是| C[直接复用]
    B -->|否| D[新建连接<br>→内核资源竞争]
    D --> E[RT陡增]
    C --> F[稳定低延迟]

2.3 SetMaxOpenConns/SetMaxIdleConns/SetConnMaxLifetime的协同调优实践

数据库连接池三参数需联动调优,孤立设置易引发资源争用或连接泄漏。

三参数语义与依赖关系

  • SetMaxOpenConns: 全局最大并发连接数(含活跃+空闲),硬性上限
  • SetMaxIdleConns: 空闲连接池容量,影响复用效率与内存开销
  • SetConnMaxLifetime: 连接最大存活时长,强制淘汰老化连接防 stale state

典型安全配比(PostgreSQL 场景)

参数 推荐值 说明
MaxOpenConns 50 基于DB最大连接数(如 pg max_connections=100)预留余量
MaxIdleConns 20 ≈ MaxOpenConns × 0.4,平衡复用率与GC压力
ConnMaxLifetime 30m 小于DB端 tcp_keepalive_time,规避防火墙中断
db.SetMaxOpenConns(50)
db.SetMaxIdleConns(20)
db.SetConnMaxLifetime(30 * time.Minute) // 防止连接被中间设备静默断连

此配置使连接池在高并发下保持约20个热连接待命,其余按需创建/销毁;30分钟生命周期确保连接在NAT超时前主动轮换,避免“connection reset by peer”。

调优验证流程

  1. 启用 sql.DB.Stats() 监控 OpenConnections, Idle
  2. 模拟峰值流量,观察 WaitCount 是否持续增长(→ MaxOpenConns 过小)
  3. 检查 MaxOpenConnections 是否频繁达上限(→ 需扩容或优化SQL)
graph TD
    A[应用请求] --> B{连接池有空闲连接?}
    B -->|是| C[复用空闲连接]
    B -->|否且<MaxOpenConns| D[新建连接]
    B -->|否且已达上限| E[阻塞等待或超时]
    C & D --> F[执行SQL]
    F --> G[归还连接]
    G --> H{连接超时?}
    H -->|是| I[销毁连接]
    H -->|否| B

2.4 连接泄漏的定位方法:pprof+sql.DB.Stats深度追踪

pprof 实时抓取数据库连接堆栈

启用 net/http/pprof 后,可通过 /debug/pprof/goroutine?debug=2 定位长期持有 *sql.conn 的 goroutine:

// 在 main 函数中注册 pprof
import _ "net/http/pprof"
go func() { http.ListenAndServe("localhost:6060", nil) }()

该代码启动调试服务;debug=2 显示完整调用栈,可识别未被 defer db.Close() 或 rows.Close() 释放的协程。

sql.DB.Stats 提供关键指标

调用 db.Stats() 获取实时连接状态:

Metric 含义
OpenConnections 当前已建立的活跃连接数
InUse 正被查询/事务占用的连接数
Idle 空闲等待复用的连接数

深度追踪流程

graph TD
A[pprof 发现阻塞 goroutine] --> B[定位未 Close 的 Rows/Stmt]
B --> C[检查 defer 缺失或 panic 跳过]
C --> D[结合 Stats.InUse 持续增长确认泄漏]

2.5 多租户/分库场景下连接池隔离策略与资源争抢规避

在多租户或分库架构中,共享连接池极易引发跨租户资源争抢与故障扩散。核心解法是逻辑隔离 + 物理隔离分级治理。

连接池分层隔离模型

  • 租户级隔离:为高SLA租户分配独占连接池(如 TenantA-HighPool)
  • 分组共享池:低频中小租户按业务域共享带配额的池(如 Shared-Order-Pool)
  • 兜底共享池:仅用于降级场景,启用强熔断与排队限流

HikariCP 配置示例(租户独占池)

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://shard01:3306/tenant_a?useSSL=false");
config.setPoolName("TenantA-Connection-Pool"); // 关键:显式命名便于监控
config.setMaximumPoolSize(20);   // 按租户QPS峰值×1.5预估
config.setConnectionTimeout(3000); // 避免阻塞传播
config.setLeakDetectionThreshold(60000); // 检测连接泄漏

逻辑分析:poolName 是指标打标关键,Prometheus可按名称聚合租户级连接数、等待时间;leakDetectionThreshold 设为60秒,覆盖典型慢SQL阈值,防止连接泄漏拖垮池。

隔离策略对比表

策略 故障隔离性 资源利用率 运维复杂度 适用场景
全局共享池 ❌ ⭐⭐⭐⭐⭐ ⭐ 单租户或POC验证
租户独占池 ✅ ⭐⭐ ⭐⭐⭐⭐ 金融/政务等高保障租户
分组共享+配额 ✅✅ ⭐⭐⭐⭐ ⭐⭐⭐ 中小SaaS租户混合部署

运行时资源争抢防护流程

graph TD
    A[请求进入] --> B{租户标识解析}
    B --> C[路由至对应连接池]
    C --> D[池内空闲连接?]
    D -- 是 --> E[直接分配]
    D -- 否 --> F[触发配额检查]
    F -- 未超限 --> G[排队等待]
    F -- 已超限 --> H[返回503+Retry-After]

第三章:SQL构造与执行层面的隐性开销

3.1 单条INSERT vs 多值INSERT vs REPLACE INTO的吞吐量对比实验

为量化写入效率差异,我们在 MySQL 8.0.33(InnoDB,buffer_pool=4G)上对 10 万行测试数据执行三类操作,重复 5 次取中位数:

写入方式 平均耗时(ms) QPS 主键冲突处理
INSERT INTO t VALUES (1,'a')(单条) 28400 ~352 报错终止
INSERT INTO t VALUES (1,'a'),(2,'b'),...(多值,1000/批) 1920 ~5208 报错终止
REPLACE INTO t VALUES (1,'a'),(2,'b'),...(同批) 2360 ~4237 删除+插入
-- 多值INSERT示例(批量1000行)
INSERT INTO users (id, name, email) VALUES
  (1, 'Alice', 'a@x.com'),
  (2, 'Bob',   'b@x.com'),
  -- ... 共1000组
  (1000, 'Zoe', 'z@x.com');

▶ 逻辑分析:单条INSERT因网络往返+解析开销高;多值INSERT显著降低语句解析与事务日志刷盘频次;REPLACE INTO额外触发唯一键匹配与可能的DELETE,故略慢于纯INSERT但具备幂等性。

性能关键因子

  • 批量大小:1000 行/批在吞吐与内存占用间取得平衡
  • 事务粒度:所有测试均包裹在 BEGIN; ... COMMIT; 中
  • 索引影响:二级索引越多,REPLACE 的删除成本越明显

3.2 预处理语句(Prepare)在批量写入中的真实收益与内存陷阱

批量写入的朴素实现 vs 预处理优化

直接拼接 SQL 字符串执行批量插入易引发 SQL 注入,且每次编译执行计划开销大:

-- ❌ 每次执行都需解析、校验、生成执行计划
INSERT INTO users (name, age) VALUES ('Alice', 25);
INSERT INTO users (name, age) VALUES ('Bob', 30);

预处理语句的核心价值

使用 PREPARE + EXECUTE 复用执行计划,显著降低 CPU 开销:

-- ✅ 一次编译,多次执行(PostgreSQL 示例)
PREPARE insert_user (text, int) AS 
  INSERT INTO users (name, age) VALUES ($1, $2);
EXECUTE insert_user ('Alice', 25);
EXECUTE insert_user ('Bob', 30);

逻辑分析:$1/$2 是占位符,由客户端驱动绑定参数;服务端仅缓存执行计划,避免重复语法树构建与权限校验。但注意:预处理语句在连接生命周期内常驻内存,长期未 DEALLOCATE 将累积内存泄漏。

内存陷阱警示

场景 内存影响 规避方式
高频 PREPARE 未释放 单连接内存持续增长 使用连接池自动 DEALLOCATE ALL
动态生成大量不同 SQL 模板 执行计划缓存爆炸 统一参数化模板,避免 WHERE id = ? 和 WHERE status = ? 混用
graph TD
  A[客户端发起 PREPARE] --> B[服务端编译并缓存执行计划]
  B --> C[后续 EXECUTE 复用计划]
  C --> D{连接关闭?}
  D -- 否 --> E[内存持续占用]
  D -- 是 --> F[自动清理]

3.3 NULL值、时间类型、JSON字段序列化引发的驱动层额外拷贝分析

驱动层拷贝的触发场景

JDBC驱动(如 MySQL Connector/J 8.0+)在处理 NULL、TIMESTAMP 和 JSON 字段时,为保障类型安全与协议兼容性,会执行隐式深拷贝:

  • NULL 值:ResultSet.getObject() 对 NULL 返回 null,但内部仍需构造空占位对象以对齐列元数据;
  • 时间类型:TIMESTAMP 被反序列化为 LocalDateTime 时,驱动默认启用 serverTimezone 时区转换,触发 Calendar 实例克隆;
  • JSON 字段:MySQL 的 JSON 类型返回 String,但若启用 cacheDefaultTimeZone=false,驱动会额外 new String(bytes) 防止字节数组被复用。

典型拷贝开销示例

// ResultSetImpl.java(Connector/J 源码简化)
public Object getObject(int columnIndex) throws SQLException {
    byte[] data = getBytes(columnIndex); // 原始字节缓冲区
    if (data == null) return null;
    // ⚠️ 即使是 JSON 字段,也强制拷贝避免 buffer 复用污染
    return new String(data, StandardCharsets.UTF_8); // ← 额外拷贝点
}

该行导致每次 getObject() 调用产生一次 String 构造开销,尤其在批量同步场景下放大内存压力。

拷贝行为对比表

字段类型 是否触发拷贝 触发条件 拷贝目标
NULL 是 getObject() 调用 空 Object 占位
DATETIME 是 serverTimezone 启用 Calendar 克隆
JSON 是 默认配置(未设 noAccessToProcedureBodies) String 实例

优化路径示意

graph TD
    A[原始字节流] --> B{字段类型判断}
    B -->|NULL| C[返回 null,但填充元数据 slot]
    B -->|TIME/TIMESTAMP| D[创建 Calendar → clone()]
    B -->|JSON| E[new String(bytes)]
    C --> F[内存引用无拷贝,但 slot 占用仍存在]

第四章:事务控制与一致性保障的典型反模式

4.1 全量大事务导致锁等待超时与主从延迟爆炸的现场复现

数据同步机制

MySQL 主从复制基于 binlog 的逻辑复制,大事务会生成单条超长 binlog event,从库 SQL 线程需原子性回放,无法并行拆分。

复现关键步骤

  • 构造含 50 万行 UPDATE 的单事务
  • 设置 innodb_lock_wait_timeout = 3(秒)
  • 并发执行多线程读写竞争同一行
-- 模拟全量更新大事务(生产环境严禁如此)
BEGIN;
UPDATE orders SET status = 'shipped' WHERE created_at < '2023-01-01';
COMMIT; -- 执行耗时约 87s

此语句在 InnoDB 中持锁时间 ≈ 扫描+更新总耗时,期间阻塞所有 SELECT ... FOR UPDATE 及 DML;autocommit=OFF 下事务未提交即持续占用行锁与间隙锁。

延迟量化对比

指标 正常场景 大事务期间
Seconds_Behind_Master 0~0.2s 突增至 320s+
Innodb_row_lock_time_avg 12ms 跃升至 4800ms
graph TD
    A[主库提交大事务] --> B[写入巨型binlog event]
    B --> C[从库SQL线程串行回放]
    C --> D[堆积 relay log + 延迟飙升]
    D --> E[后续小事务被阻塞在apply queue]

4.2 分块提交(Chunked Commit)的粒度选择:100行?1000行?还是动态自适应?

粒度影响维度

分块大小直接影响事务吞吐、锁持有时间与内存开销。固定粒度(如100/1000行)难以适配异构负载。

动态自适应策略

def calc_chunk_size(last_latency_ms: float, target_ms=50) -> int:
    # 基于上一轮执行延迟动态调整:目标单块耗时≈50ms
    base = 500
    ratio = max(0.3, min(3.0, target_ms / max(1, last_latency_ms)))
    return max(50, min(5000, int(base * ratio)))

逻辑分析:以历史执行延迟为反馈信号,线性缩放基础块大小;max/min确保安全边界,避免极端抖动导致OOM或长事务。

对比基准(TPS & 平均延迟)

粒度 平均延迟(ms) 吞吐(ops/s) 锁冲突率
100行 12.4 842 2.1%
1000行 98.7 1023 18.6%
自适应 47.3 1156 5.3%

执行路径示意

graph TD
    A[读取批数据] --> B{是否达阈值?}
    B -->|否| C[累积至缓冲区]
    B -->|是| D[触发提交+重置计时]
    D --> E[记录本次耗时]
    E --> F[更新下一块目标大小]

4.3 事务内错误回滚不完整导致部分写入的脏数据检测与补偿方案

数据同步机制

当分布式事务因网络超时或服务崩溃中断,JDBC连接可能仅回滚部分SQL,留下跨表不一致状态。例如订单表已提交,但库存表回滚失败。

脏数据识别策略

  • 基于事务日志(如MySQL binlog)提取未完成事务的XID与修改行主键
  • 结合应用层埋点(@Transactional切面记录事务边界)交叉验证

补偿执行示例

// 基于Saga模式的幂等补偿
public void compensateOrderInventory(String orderId) {
    // 1. 查询订单当前状态(避免重复补偿)
    OrderStatus status = orderMapper.selectStatus(orderId); 
    if (status == CANCELLED) return;

    // 2. 恢复库存(乐观锁防止并发覆盖)
    int affected = inventoryMapper.restoreStock(
        orderId, 
        1, // 版本号,确保仅补偿一次
        LocalDateTime.now()
    );
}

逻辑分析:restoreStock方法通过version字段实现幂等性;参数1为初始补偿版本号,后续重试自动递增,避免重复扣减。

检测维度 技术手段 实时性
事务完整性 XA事务日志解析 秒级
数据一致性 主键哈希校验+定时扫描 分钟级
业务语义合规性 订单/库存状态联合规则引擎 毫秒级
graph TD
    A[事务中断] --> B{是否记录XID?}
    B -->|是| C[解析binlog定位未回滚SQL]
    B -->|否| D[触发全量一致性扫描]
    C --> E[生成补偿任务]
    D --> E
    E --> F[幂等执行补偿]

4.4 Read Committed隔离级别下幻读对批量更新结果集的影响与规避

在 READ COMMITTED 隔离级别下,事务每次执行 SELECT 都会获取最新已提交快照,但不锁定间隙(gap),导致幻读——新插入的满足条件行可能被后续 UPDATE ... WHERE 捕获,造成非预期批量更新。

幻读引发的批量更新偏差示例

-- 事务A(执行批量更新)
UPDATE orders SET status = 'processed' 
WHERE created_at > '2024-01-01' AND status = 'pending';
-- 此时事务B插入一条新pending订单 → A的UPDATE将额外影响该行

逻辑分析:READ COMMITTED 不加间隙锁,WHERE 条件扫描基于当前快照,但 UPDATE 实际执行时重新评估行可见性,新插入行若已提交即被纳入更新范围。参数 created_at 和 status 构成非唯一查询条件,加剧幻读风险。

规避策略对比

方案 是否阻塞插入 是否需应用层改造 适用场景
升级为 REPEATABLE READ 是(间隙锁) 否 高一致性要求、低并发写
SELECT ... FOR UPDATE 显式加锁 是 是 精确控制更新边界
基于主键分批更新 否 是 大表、高吞吐场景

推荐实践:主键锚定+时间窗口校验

-- 先获取ID列表(快照一致),再按ID精确更新
WITH pending_ids AS (
  SELECT id FROM orders 
  WHERE created_at > '2024-01-01' 
    AND status = 'pending'
  ORDER BY id LIMIT 1000
)
UPDATE orders 
SET status = 'processed' 
WHERE id IN (SELECT id FROM pending_ids) 
  AND status = 'pending'; -- 双重校验防竞态

逻辑分析:CTE 提前固化ID集合,避免WHERE条件重求值;末尾 AND status = 'pending' 防止其他事务并发修改导致误更新。LIMIT 1000 控制锁粒度与事务时长。

graph TD A[启动批量更新] –> B[快照读取匹配行ID] B –> C[按ID列表执行UPDATE] C –> D[双重status校验] D –> E[提交/回滚]

第五章:Go-sql-driver优化配置清单与生产环境落地checklist

连接池参数调优实践

在高并发电商订单服务中,我们将 maxOpenConns=50、maxIdleConns=30、connMaxLifetime=1h 作为基线配置。压测发现当 maxOpenConns 低于25时,P99延迟突增320ms;而设为100后,MySQL端出现大量 Too many connections 错误。最终通过Prometheus+Grafana监控连接池使用率(sql_conn_used / sql_conn_max),将值稳定控制在65%±5%,兼顾资源复用与突发流量缓冲。

DSN安全增强配置

生产环境强制启用以下DSN参数组合:

user:password@tcp(10.20.30.40:3306)/dbname?parseTime=true&loc=Asia%2FShanghai&timeout=5s&readTimeout=10s&writeTimeout=10s&interpolateParams=true&multiStatements=false&charset=utf8mb4

特别注意:interpolateParams=true 需配合预编译禁用(sql.DB.Prepare() 显式调用),避免SQL注入风险;multiStatements=false 防止恶意分号注入。

死锁与超时熔断策略

建立三层超时防护机制:

层级 配置项 生产值 触发动作
应用层 context.WithTimeout() 3s 返回HTTP 503,记录traceID
驱动层 readTimeout/writeTimeout 10s 关闭socket连接
MySQL层 wait_timeout 28800s 由DBA统一维护

当连续5分钟死锁率 > 0.8%,自动触发告警并执行 SHOW ENGINE INNODB STATUS 快照采集。

TLS加密强制校验流程

graph TD
    A[应用启动] --> B{env == production?}
    B -->|是| C[加载CA证书链]
    B -->|否| D[跳过TLS验证]
    C --> E[DSN添加tls=custom]
    E --> F[验证MySQL服务器证书CN匹配域名]
    F --> G[拒绝自签名证书握手]

监控埋点关键指标

  • sql_conn_open_total(累计打开连接数)
  • sql_query_duration_seconds_bucket(按SQL类型分桶的P95耗时)
  • sql_txn_rollback_total(事务回滚率,>3%触发DBA介入)
  • sql_prepared_stmt_cache_hit_ratio(预编译缓存命中率,

故障应急操作手册

某次K8s滚动更新导致连接泄漏:

  1. kubectl exec -it <pod> -- netstat -an \| grep :3306 \| wc -l 确认连接数达1200+
  2. 执行 curl http://localhost:6060/debug/pprof/goroutine?debug=2 发现database/sql.(*DB).connectionOpener goroutine堆积
  3. 紧急调整 SetConnMaxLifetime(30s) 并重启Pod
  4. 后续在CI阶段加入连接泄漏检测:go test -gcflags="-l" -vet=atomic ./...

字符集与时间处理规范

所有生产服务必须设置 charset=utf8mb4&collation=utf8mb4_unicode_ci,禁止使用utf8别名。时间字段统一采用time.Time类型,DSN中parseTime=true开启解析,数据库列定义为DATETIME(6)以支持微秒精度,避免NOW()与time.Now()时区偏差。

连接健康检查脚本

#!/bin/bash
# 每30秒探测连接池活性
if ! mysqladmin -u$USER -p$PASS -h$HOST ping --silent; then
  echo "$(date): MySQL unreachable, triggering failover" | logger -t go-sql-driver
  curl -X POST http://consul:8500/v1/health/service/go-mysql?passing=false
fi

分享 Go 开发中的日常技巧与实用小工具。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注