无损滚动更新机制

想要进行无损滚动更新需要保证一下3点 应用支持优雅退出(收到退出信号时不接收新请求并且要处理完已经接收到的请求) k8s正确配置readnessProbe就绪探针 添加生命周期函数preStop添加一定的延时, 比如preStop.exec.commend: ["/bin/sh", “-c”, “sleep 10”] 主要原因为Kubernetes 在终止 Pod 时的操作顺序是 标记 Pod 为 Terminating 状态:当 Kubernetes 接收到删除 Pod 的请求时,它会将 Pod 的状态设置为 Terminating 执行 preStop 钩子:如果 Pod 定义了 preStop 钩子,Kubernetes 会立即同步执行这个钩子 并行更新 Endpoint:在 Kubernetes 标记 Pod 为 Terminating 状态的同时,Endpoint Controller 会异步地从相关的服务端点(endpoints)中移除该 Pod 的 IP 地址。Kube-proxy 也会开始更新其网络规则,以停止向该 Pod 转发流量 发送 SIGTERM 信号:一旦 preStop 钩子完成,Kubernetes 会向 Pod 中的容器发送 SIGTERM 信号,告诉容器开始优雅地关闭 等待或强制终止:如果容器在终止宽限期(terminationGracePeriodSeconds)内没有关闭,Kubernetes 会发送 SIGKILL 信号强制终止容器。 添加延迟的原因主要是preStop和更新endpoint是异步进行,如果没有设置preStop就会出现修改endpoint和发送SIGTERM异步进行, 当应用收到SIGTERM时就不会接收新请求处理完老请求后退出,但是此时endpoint还有可能没有修改完成,就会导致一部分请求流入了即将退出的pod,而且无法响应请求,这时候这部分请求就会损失 没有配置preStop的情况 当应用接收到SIGTERM就不处理新请求了,从收到SIGTERM到更新完endpoint前,这部分请求都会异常 有配置preStop的情况 这时候能保证应用收到SIGTERM前endpoint已经更新完成,这样就不会异常了 ...

<span title='2024-09-11 15:44:16 +0800 +0800'>九月 11, 2024</span>

Kafka使用kraft搭建集群

主机准备 主机 IP 系统 kafka1 192.168.10.101 ubuntu22.04 kafka2 192.168.10.102 ubuntu22.04 kafka3 192.168.10.103 ubuntu22.04 主机环境 安装java 11 kafka_2.13-3.7.0 配置 这里部署的是broker和controller在一起的方式,也可以分别部署,具体修改kraft相关文件,配置都是相通的 kafka/config/kraft/server.properties #集群角色 process.roles=broker,controller #每台机器的id不能一样 node.id=3 #这个是所有conntroller的地址,用于投票选举使用 controller.quorum.voters=1@192.168.10.101:9093,2@192.168.10.102:9093,3@192.168.10.103:9093 # 监听端口 listeners=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 # 客户端连接地址 advertised.listeners=PLAINTEXT://192.168.20.208:9092 # 日志地址, 这个修改到非/tmp目录,否则系统重启后会丢失 log.dirs=/var/log/kafka/kraft-combined-logs # ...其他配置按照实际情况修改 启动集群步骤: KAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)" 这个操作只在一个机器上运行,执行后记录这个值,这个是集群id,整个集群需要一致 bin/kafka-storage.sh format -t $KAFKA_CLUSTER_ID -c config/kraft/server.properties 每个机器上都要执行, 这个KAFKA_CLUSTER_ID 就是是上面执行的值 bin/kafka-server-start.sh config/kraft/server.properties 启动集群 配置systemd开机启动 在/etc/systemd/system/kafka.service文件中添加内容 [Unit] Description=Apache Kafka Server [Service] Type=simple User=root Group=root ExecStart=/usr/local/kafka/bin/kafka-server-start.sh /usr/local/kafka/config/kraft/server.properties ExecStop=/usr/local/kafka/bin/kafka-server-stop.sh Restart=on-failure [Install] WantedBy=multi-user.target 执行sudo systemctl daemon-reload 重新加载 执行sudo systemctl enable kafka 开机启动 执行sudo systemctl start kafka 启动 使用sudo journalctl -u kafka.service查看日志 使用jps查看进程是否启动成功 ...

<span title='2024-07-04 15:16:44 +0800 +0800'>七月 4, 2024</span>

Debezium

Debezium 用于捕获数据库中的更改,以便应用程序可以查看这些更改并对其做出响应。 Debezium 会记录每个数据库表中所有行级别的更改,并将其作为更改事件流记录下来 主流CDC工具: canal : 监控binlog,支持mysql,社区已不活跃,非首选 debezium : 支持mysql, postgresql, mongodb等十余种数据库,mysql也支持binlog,成熟稳定 flink cdc: 主要应用于大数据,基于debezium,延迟较低 这里选择debezium的理由: 不选canal的原因 只支持mysql,并且社区已不活跃,除了老项目使用新的已经不是首选,在阿里也都边缘化了 不选flink cdc的原因,公司大数据没有flink的项目,增加运维成本,并且需要使用java开发,并不是所有开发都会使用java 选择debezium的原因,debezium成熟稳定,支持多种数据库,经典的使用方式是使用的是kafka connect部署, 我们有自己的kafka,不会增加运维成本, kafka connect是kafka自带的分布式工具,不用搭建额外的平台管理任务,使用消息队列和固定语言解耦,降低开发成本 当我们想监控数据库数据变动时我可以使用一些CDC工具,这里介绍一款配合Kafka使用的Connect插件Debezium,他支持Mysql, PostgreSQL, MongoDB等 能做什么: 1. 解耦数据修改端和使用端,减少应该通知但是没有通知的数据变更的错误,或者手动修改数据库时没有或忘记通知其他端 2. 异构数据库同步数据,比如搜索相关业务,需要数据实时同步,但是要保证数据一致 3. 缓存数据的生成和过期 4. 一些计算任务(数据统计,聚合) 这里我们以MySQL为例 环境 1. Kafka 3.7 2. MySQL 8.0 3. Debezium Mysql connect Plugin 2.6.2 Final Mysql 需要开启row格式binlog [mysqld] bind-address = 0.0.0.0 binlog_format = ROW server_id = 1 log_bin = /var/log/mysql/mysql-bin.log 需要以下权限 GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'user' IDENTIFIED BY 'password'; 权限 作用 SELECT 查询数据,仅在执行快照时使用 RELOAD 允许连接器使用 FLUSH 语句来清除或重新加载内部缓存、刷新表或获取锁。仅在执行快照时使用 SHOW DATABASES 使连接器能够通过发出 SHOW DATABASE 语句来查看数据库名称。仅在执行快照时使用 REPLICATION SLAVE 使连接器能够连接并读取 MySQL 服务器二进制日志 REPLICATION CLIENT 允许连接器使用以下语句:SHOW MASTER STATUS,SHOW SLAVE STATUS,SHOW BINARY LOGS LOCK TABLES 执行快照时需要锁表 Debezium 把插件解压到一个目录下,我这里解压到Kafka目录下,创建一个connects目录,解压到这里,解压后我的目录像这样 --kafka |--connects |--debezium-connector-mysql 在kafka的config文件夹下配置connect-distributed.properties这个文件 修改plugin.path填写connects文件夹路径 ...

<span title='2024-06-28 11:15:28 +0800 +0800'>六月 28, 2024</span>

Deadlock检测时机

Go语言的运行时会在所有活跃的goroutine都无法继续执行时判定为死锁。这意味着,只要还有至少一个goroutine能够正常运行,Go运行时就不会触发死锁检测机制。死锁检测通常在以下情况下触发: 所有goroutine都阻塞:如果所有的goroutine都在等待某些事件(如通道操作、锁获取等),而这些事件无法由其他goroutine触发(因为没有其他goroutine在运行或者能够解除阻塞状态),Go运行时就会判定程序为死锁状态。(这里不包括io等待,因为io等待是阻塞的,但是go的运行时并不会触发死锁检测机制) 这种自动死锁检测主要是为了帮助开发者在开发阶段识别出潜在的并发问题。然而,它的能力是有限的,特别是在涉及网络I/O、系统调用或者复杂锁逻辑的情况下,Go的死锁检测可能不会触发。因此,即使Go运行时没有报告死锁,也不代表程序中不存在潜在的并发问题。 为了避免死锁,推荐的做法包括: 避免循环等待:设计系统时应确保资源的分配顺序一致,以避免循环等待的情况发生。 使用适当的同步原语:比如使用带缓冲的channel、正确使用锁(如sync.Mutex)、以及其他并发控制工具(如sync.WaitGroup、context.Context等)。 限制并发数:有时通过限制系统中并发执行的goroutine数量可以简化资源管理,减少死锁的风险。 彻底测试:并发程序应该经过详尽的测试,包括使用竞态检测工具(如Go的-race标志)来帮助识别并发错误。 这些措施可以帮助开发者构建更健壮、更可靠的并发程序。

<span title='2024-05-08 17:52:55 +0800 +0800'>五月 8, 2024</span>

在docker模拟虚拟IP实验时遇到的问题

获取不到虚拟IP 使用下面的命令时不能获取到虚拟IP docker run -it --net vipnet --ip 172.18.0.2 vipimage /bin/bash 应该添加参数可以让容器有权限操作网络配置 docker run -it --net vipnet --ip 172.18.0.2 --cap-add=NET_ADMIN vipimage /bin/bash 或者 docker run -it --net vipnet --ip 172.18.0.2 --privileged vipimage /bin/bash privileged小知识 在 Docker 中使用 --privileged 标志会给容器提供类似于宿主机 root 用户的权限。当容器以 --privileged 模式运行时,它可以绕过 Linux 内核的许多安全限制,从而获得较广泛的操作权限。这通常用于需要执行一些高级系统管理操作的场景,例如直接访问硬件设备或进行某些需要特殊权限的网络操作。 --privileged 模式的主要影响包括: 全能力(Capabilities): 容器将获得 Linux 所有的 capabilities,与在宿主机上运行的进程几乎相同。 设备访问: 容器可以访问和操作宿主机上的所有设备(/dev 下的设备)。 安全限制: 绕过了 AppArmor 或 SELinux 的限制,容器可以执行更多的系统级操作。 文件系统: 容器可以挂载宿主机上的文件系统,甚至使用一些通常需要更高权限的挂载选项。 网络操作: 允许执行一些通常受限的网络操作,如更改网络配置或使用低号端口。 使用场景 使用 --privileged 模式通常适用于以下场景: 开发和测试: ...

<span title='2024-04-26 18:12:19 +0800 +0800'>四月 26, 2024</span>

Wine

使用flatpak安装wine后执行winecfg配置wine时出现乱码可以使用命令安装字体解决 sudo apt-get install fonts-wqy-zenhei fonts-wqy-microhei

<span title='2024-04-22 16:42:34 +0800 +0800'>四月 22, 2024</span>

添加字段引发的不兼容

大家都是知道如果我们在接口返回值添加字段或者proto中添加字段一般是不会发生不兼容的情况,那么在数据库中给表添加字段会导致不兼容问题吗? 通常情况是不会,添加字段不会影响到已有的数据,但是下面这种情况会引发不兼容 假设A表有字段id, bid, c, B表有id, d,业务中有这么一条语句 select a.id, b.d from a join b on a.bid = b.id where c = 1; 那么现在我们给B表加字段c,那么现在这个语句就会错,因为a表有个c,b表有个c,现在where条件中的c已经有歧义了,所以这条语句就会报错,所以给表加字段会引发不兼容 所以加字段的时候也要小心,sql如果是多表一定给字段加上别名,避免这种歧义就比如上面的语句就改为 select a.id, b.d from a join b on a.bid = b.id where a.c = 1; 这只能说约束我们自己,历史的老系统中有什么样的写法都未可知

<span title='2024-01-12 12:37:19 +0800 +0800'>一月 12, 2024</span>

mysql中time_zone的作用,以及为什么要配置本地时区和db时区

mysql中time_zone的作用,以及为什么要配置本地时区和db时区 1. mysql中time_zone的作用 mysql中time_zone的作用,就是用来设置时区的(废话),time_zone影响TIMESTAMP类型和NOW等函数的值,但并不影响DATETIME类型和DATE类型。 因为TIMESTAMP存储的时时间戳,当用户发送过来一个时间如“2023-12-11 10:41:18”,那么mysql会将其时间戳,这个时间就是按time_zone设置的时区来解析的,然后转换成时间戳保存, 在给用户查询时在通过时区转换会时间串。 DATETIME类型不是时间戳,存入时是“2023-12-11 10:41:18”,查询时也是“2023-12-11 10:41:18”,不会做转换,也和时区无关 2. 为什么要配置本地时区和db时区 本地时区是给mysql驱动使用,并不会影响mysql的时区,只会影响mysql驱动的解析, db时区是mysql的时区,会影响mysql的时区,会影响mysql的查询结果。 本地时区作用 在读取到mysql发来的时间,go会按照本地时区来解析,转换为time.Time类型mysql按照数据库time_zone返回时间串后,go并不知道用哪个时区来解析这个时间串,所以需要设置本地时区。 本在写入time.Time类型时,go会把time.Time转换为本地时区发送给mysql 比如设置本地时区为“Asia/Shanghai” root:root@tcp(127.0.0.1:3306)/db?charset=utf8&loc=Asia%2FShanghai&parseTime=true通过loc参数来这设置 同时设置本地和数据库时区 root:root@tcp(127.0.0.1:3306)/db?charset=utf8&loc=Asia%2FShanghai&parseTime=true&time_zone=%27%2B8%3A00%27 dsn上的参数有的是给数据库驱动使用,有的是给mysql使用,具体可参阅mysql驱动的DSN

<span title='2023-12-11 10:41:18 +0800 +0800'>十二月 11, 2023</span>

本地+redis多级缓存

由一次线上问题引发的思考,本地缓存+redis缓存的多级缓存方案。 一次线上发生OOM, CPU占用高排查,之前所有的缓存都是放在redis中,并发高时,大量请求去redis,服务反序列化,导致CPU占用高,内存占用高,最终达到资源上线被k8s杀掉。 后来想到了使用本地缓存,这种公共对象保存一份数据,不用反复序列化,减少CPU占用,减少内存占用,但是本地缓存有一个问题,多个副本在同一时间可能缓存数据不一致,虽然在我们这个场景下,这份公共数据更新不频繁,但是也有可能发生这个情况,所以想到了使用多级缓存,本地缓存+redis缓存,本地缓存作为一级缓存,redis作为二级缓存,当redis更新时会设置数据版本号(时间戳),本地获取时会比对版本号,如果相同redis就不返回数据,如果不同就返回数据,这样就可以保证数据一致性。 用go描述一下取值和设置值的逻辑,其他逻辑都比较简单,这里使用lua脚本来实现,版本和数据的原子性和比较版本和返回相应的返回值,还可以减少网络开销 package main import ( "fmt" "time" "github.com/go-redis/redis" ) //这段脚本是用来设置值的,设置值的时候会设置版本号,设置过期时间 const setval = ` local key_val = KEYS[1] local key_version = key_val .. "_version" local val = ARGV[1] local expire = ARGV[2] local version = ARGV[3] redis.call('SET', key_version, version) redis.call('EXPIRE', key_version, expire) redis.call('SET', key_val, val) redis.call('EXPIRE', key_val, expire) return nil ` //这段脚本是用来取值的,取值的时候会比较版本号,如果版本号不一致就返回值 const getval = ` local key_val = KEYS[1] local key_version = key_val .. "_version" local givenVersion = ARGV[1] local version = redis.call('GET', key_version) if not version then return nil end if version ~= givenVersion then local value1 = redis.call('GET', key_val) return {version, value1} end return {version} ` func main() { redisClient := redis.NewClient(&redis.Options{ Addr: "xxx", Password: "xxx", DB: 0, }) cmd := redisClient.Eval(getval, []string{"ab"}, "1") if cmd.Err() != nil { fmt.Println(cmd.Err()) } val := cmd.Val() fmt.Println(val) cmd = redisClient.Eval(setval, []string{"ab"}, time.Now().UnixNano(), 100, time.Now().Unix()) if cmd.Err() != nil && cmd.Err() != redis.Nil { fmt.Println(cmd.Err()) } } 说一下总体的思路 使用一个localstorage, redis, redis lua, singleflight ...

<span title='2023-11-24 14:20:35 +0800 +0800'>十一月 24, 2023</span>

DCDC原理 电感

DCDC 升压原理 直流-直流(DC-DC)升压转换器是一种电子电路,用于将输入直流电压转换为较高的输出直流电压。其原理基于能量储存和传递的概念。 DC-DC升压转换器的基本原理: 开关器件: 这种转换器通常使用开关器件(如MOSFET)来控制输入电压的存储和传递。这些器件通过开关操作,控制能量流动的路径,实现电压转换。 储能元件: 其中一个核心组件是储能元件,比如电感或电容。在升压转换器中,电感是最常见的储能元件之一。当开关器件关闭时,电感储存电能;当开关器件打开时,储能元件释放储存的能量,以提供输出所需的较高电压。 控制电路: 升压转换器还包括一个控制电路,用于监测输出电压并控制开关器件的工作周期,以保持所需输出电压稳定。 脉宽调制(PWM): 控制电路通常使用脉宽调制技术,通过改变开关器件的工作周期和频率来调节输出电压。通过调整开关器件的工作时间和间隔,可以控制能量传递的速率,从而实现输出电压的精确调节。 在升压转换器中,输入电压经过周期性开关,并通过储能元件转换为较高的输出电压。这种转换方式允许在电子设备中提供所需的不同电压,例如从电池供电的设备需要较高的工作电压。 总的来说,DC-DC升压转换器通过开关器件、储能元件和控制电路来转换输入电压为所需的更高输出电压,使得在各种应用中能够实现稳定的电源供应。 如何控制电感储能后的电压 控制电感储能后的电压通常通过调整开关器件的工作周期和频率来实现。这涉及到脉宽调制(PWM)技术,通过改变开关器件的导通和关断时间,控制能量的传递速率,从而调节输出电压。 以下是实现这种控制的基本步骤: 脉宽调制(PWM): 使用PWM技术来调节开关器件的导通和关断时间。导通时间越长,电感中储存的能量就越多,从而提高输出电压。关断时间的调节则影响能量传递的速率。 控制电路: 设计一个控制电路,该电路监测输出电压并与设定值进行比较。根据比较的结果,调整PWM信号,以使输出电压保持在所需的水平。 反馈回路: 在控制电路中加入反馈回路,用于实时监测输出电压。一种常见的反馈方式是采用电压反馈,其中输出电压与参考电压进行比较,然后调整PWM信号以纠正任何电压偏差。 PID控制: 对于更复杂的系统,可能采用比例-积分-微分(PID)控制器。PID控制器可以更精确地调节输出电压,通过对比例项、积分项和微分项的权衡来实现快速而稳定的响应。 通过这些控制手段,可以实现对电感储能后的电压进行精确调节,使其保持在所需的水平,从而满足不同应用对电源电压的要求。 如何理解通过PWM来调节电感电压,举例说明 脉宽调制(PWM)是一种通过调整脉冲的宽度来控制平均电压的技术。在电感储能的情况下,PWM被用来调节开关器件的工作周期,从而影响电感中存储的能量量,最终调节输出电压。以下是一个简单的例子,说明如何通过PWM来调节电感电压: 基本电路架构: 考虑一个基本的升压DC-DC转换器,包含以下关键组件: 开关器件(例如MOSFET):用于控制电能的开关。 电感:储存能量的元件。 控制电路:监测输出电压并生成PWM信号。 工作原理: 当开关器件导通(关闭)时,电流通过电感,将能量储存在电感中。 当开关器件关断时,储存在电感中的能量通过二极管传递到输出端,提供给负载。 PWM控制: 控制电路监测输出电压,并将其与设定值进行比较。 如果输出电压低于设定值,控制电路将增加PWM信号的宽度,延长开关器件的导通时间,使电感中储存的能量增加。 如果输出电压高于设定值,控制电路将减小PWM信号的宽度,缩短导通时间,减少电感中的储能。 示例: 初始状态下,输出电压低于设定值。 控制电路增加PWM信号的宽度。 开关器件导通时间延长,电感中储存的能量增加。 输出电压逐渐上升。 控制电路监测到输出电压接近设定值,减小PWM信号的宽度。 开关器件导通时间缩短,电感中的储能减少。 输出电压稳定在设定值附近。 通过这种方式,PWM信号的调节直接影响了开关器件的工作周期,从而影响了电感中储存的能量,最终调整了输出电压。这种反馈控制系统能够在不同工作条件下稳定地提供所需的电压输出。 如何计算通断时间产生的电感电压 电感在开关电路中的通断时间会影响其产生的电压。在直流-直流(DC-DC)升压转换器中,可以使用以下基本公式来估算电感电压: $V_L = L \frac{\Delta I}{\Delta t}$ 其中: $(V_L)$ 是电感上的电压。 $(L)$ 是电感的电感值。 $(\Delta I)$ 是电感中电流的变化。 $(\Delta t)$ 是电流变化所经过的时间。 这个公式基于电感的电压-电流关系: $V_L = L \frac{di}{dt}$ ...

<span title='2023-11-23 14:42:12 +0800 +0800'>十一月 23, 2023</span>