RoLingG | 博客 - 其他
type: channel
link: https://rolingg.top/category/%E5%85%B6%E4%BB%96/
description: 一些杂项Blog。
items: [kafka] 实操记录
items: [n8n] 部署与踩坑
items: [MySQL] InnoDB和MyISAM对于锁粒度的区别
items: [Redis] string大数据量如何扩容
items: [Golang/Redis] Redis使用Lua脚本示例三则
items: [Golang] Map的底层实现原理与并发安全（待填坑）
items: [Golang] Channel原理
items: [Kafka] 高可用实例
items: [Kubernetes] 调度策略及相关内容
items: [面试] 腾讯csig运维实习生面经

[kafka] 实操记录
type: item
date: 2025-08-31T12:46:00+08:00
description: Kafka实操简单的一对一生产消费订阅模型生产者Producer：package main import ( "fmt" "github.com/IBM/sarama" "log" ) func main() { // 配置生产者 config := sarama.NewConfig() config.Producer.RequiredAcks = sarama.WaitForAll // 等待所有副本确认 config.Producer.Retry.Max = 3 // 重试次数 config.Producer.Return.Successes = true // 返回成功的消息 // 创建同步生产者（也可创建异步生产者） producer, err := sarama.NewSyncProducer([]string{"192.168.31.108:9092"}, config) if err != nil { log.Fatalf("Failed to create producer: %s", err) } defer producer.Close() // 发送消息 topic := "test-topic" for i := 0; i < 5; i++ { msg := &sarama.ProducerMessage{ Topic: topic, Value: sarama.StringEncoder(fmt.Sprintf("Hello Sarama! Message %d", i)), } // 同步发送，返回分区和偏移量 partition, offset, err := producer.SendMessage(msg) if err != nil { log.Printf("Failed to send message: %s", err) } else { fmt.Printf("Message sent to partition %d, offset %d\n", partition, offset) } } }消费者Consumer：package main import ( "fmt" "github.com/IBM/sarama" "log" ) func main() { // 配置消费者 config := sarama.NewConfig() config.Consumer.Return.Errors = true // 默认配置消费者偏移量 OffsetOldest: 从最早的消息开始消费 OffsetNewest:从当前最新消息开始消费 config.Consumer.Offsets.Initial = sarama.OffsetOldest // 2. 创建基础消费者（非消费组模式） consumer, err := sarama.NewConsumer([]string{"192.168.31.108:9092"}, config) if err != nil { log.Fatalf("Failed to create consumer: %s", err) } defer consumer.Close() // 3. 指定要消费的主题和分区 topic := "test-topic" partition := int32(0) // 分区编号（从0开始） // 4. 获取分区消费者（从指定偏移量开始消费） // 第三个参数为起始偏移量：可以是 sarama.OffsetOldest / sarama.OffsetNewest 或具体数值 // 无论是哪个参数都会进行持续监听当前的partition pc, err := consumer.ConsumePartition(topic, partition, sarama.OffsetOldest) if err != nil { log.Fatalf("Failed to consume partition: %s", err) } defer pc.Close() for msg := range pc.Messages() { // 成功接收到消息 fmt.Printf("%s %s %d %d\n", msg.Topic, msg.Value, msg.Partition, msg.Offset) } }多生产者单消费者大部分使用kafka的场景就是多个生产者生产消息，往kafka里面发，然后消费者在后面慢慢消费。这种模式不需要做额外配置，生产者只管往这个topic里面发就好了。举例实际应用中，有多个服务产生日志，这些服务就可以每个配置一个日志生产者，生产出来的日志信息传输进kafka，然后对应处理日志的服务作为消费者模型…

[n8n] 部署与踩坑
type: item
date: 2025-08-07T11:14:00+08:00
description: 前言：在部署前请自行解决服务器和域名相关问题。n8n 是一款开源、低代码的工作流自动化平台。部署也相当容易，可选择 npm 安装，但是相对机器性能要求比较高；也可以选择 docker 进行安装，方便快捷但是需要会 docker 相关操作。安装和部署我这边的话推荐用 docker 进行安装，比如 1panel 面板上的一键安装，或者像这位 大佬 教的进行安装。如果有需要中文版，也可以使用 n8n-i18n-chinese 该Github仓库进行根据MD文档操作进行安装，该仓库支持 1.86.1 以上的版本直接使用仓库内的 docker-compose.yml 进行快速安装，不需要自己过多的操作。操作前提示：一定要打开自己域名管理里面的 Websocket 开关。因为 n8n 中的环境变量N8N_PUSH_BACKEND决定了后端如何向 UI 发送实时更新。它可以设置为 websocket 或 sse（服务器发送事件）：默认： websocket选项：websocket：使用 WebSockets 进行推送通知和实时更新。sse：使用服务器发送事件。1panel安装部署在应用商店直接搜索 n8n ，并点击安装即可，版本自选，端口自己设置在自己想的，n8n 默认端口为 5678。但记住，从 n8n 的 1.79 版本之后，官方便对 n8n 的持续服务网络连接进行了更改，往上版本如果环境变量参数缺少了部分参数，会导致部署好之后使用时提示 Connection lost 。这不是自己操作的问题，是官方并没有明确提出的问题。# 以下是我完整的容器环境变量 N8N_SECURE_COOKIE=false # 如果想要cookie验证这里可以改成true WEBHOOK_URL=https://xxx.xxx.com/ # 缺少需补上，1.79版本以上刚需该变量 N8N_HOST=xxx.xxx.com # 缺少需补上 N8N_DEFAULT_LOCALE=zh-CN # 如果是中文版需要加上，不是的话不需要，可以无视 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NODE_VERSION=20.19.0 YARN_VERSION=1.22.22 NODE_ICU_DATA=/usr/local/lib/node_modules/full-icu N8N_VERSION=1.86.1 NODE_ENV=production N8N_RELEASE_TYPE=stable SHELL=/bin/sh之后在面板里域名配置好对应端口的反向代理就好了。Docker直接部署这里以上述中文版仓库为例，将仓库 git clone 下来之后，按照作者的 README.md 说的操作就好了。之后配置好域名对应端口 15678:5678 的反向代理就可以了。但是环境变量相关问题同上1panel安装部署的参数一样，如果是高于 1.79 的版本都需要额外写入参数，保证服务连接。部署完在这之后只需要跳转到自己 n8n 的域名，就会有注册登录的页面，弄完之后就可以快乐的玩耍了。这之后再附带一个简单的自动化实战案例。案例这里我们就实现一个简单的 Github的 Golang 仓库接收到推送后，自动同步到服务器内编译并后台运行跑起服务的案例。这里需要在对应仓库内的 Settings → Webhook 里面配置好：仓库需要配置两个 Webhook，一个是测试的，一个是生产实际用的。这里的 Payload URL 要和 n8n 工作流里的 URL 一致，测试URL 即 https://域名/webhook-test/path 这里的 path 是 n8n 的 Webhook 里你自己设置的；如果是 生产URL即 https://域名/webhook/path。这样 Github 和 n8n 工作流的 Webhook 同步配置好后，就能接通了。Webhook 就像一个触发器，这样配 Github 仓库只要有 push 事件发生，就会像对应 URL 发送 POST 请求，n8n 工作流只要处于 active 状态，就会检测到 POST 请求并自动执行之后的工作流。Webhook 触发后就到 Execute Command 即在 n8n 所在系统执行你所需要的命令，这里我因为是 docker 部署的，所以命令都在容器里。这里就需要 ssh 连接到宿主机去对应文件区域执行我们所需要的命令。这里参考两位大佬写的文章：Docker容器如何优雅地访问宿主机网络Github Actions 实现 Nuxt 3 自动构建并部署因为我是容器 → 宿主机，所以需要将密钥放在容器里，公钥在宿主机。即生成好密钥之后，…

[MySQL] InnoDB和MyISAM对于锁粒度的区别
type: item
date: 2025-07-07T11:51:04+08:00
description: MySQL的InnoDB和MyISAM对于锁粒度的区别在MySQL中，InnoDB和MyISAM是两种常用的存储引擎，它们在锁粒度（Lock Granularity）上有显著的区别。锁粒度决定了在并发操作时，锁的范围和粒度，从而影响并发性能和数据一致性。以下是InnoDB和MyISAM在锁粒度上的主要区别：1. InnoDB锁粒度：行级锁（Row-Level Locking）。特点：行级锁：InnoDB使用行级锁来管理并发操作，这意味着在执行更新或删除操作时，锁的范围仅限于被操作的行，而不是整个表。高并发性能：由于锁的粒度较小，多个事务可以同时对表中的不同行进行操作，从而提高了并发性能。支持事务：InnoDB支持事务（ACID特性），在事务中可以使用多种隔离级别（如读已提交、可重复读等），这些隔离级别通过行级锁来实现。死锁检测：InnoDB支持死锁检测，当两个或多个事务相互等待对方释放锁时，InnoDB会检测到死锁并自动回滚其中一个事务。锁的类型：共享锁（S Lock）：允许其他事务读取同一行，但不允许修改。排他锁（X Lock）：禁止其他事务读取或修改同一行。适用场景：高并发的读写操作，如电子商务系统、金融系统等。需要事务支持的应用。2. MyISAM锁粒度：表级锁（Table-Level Locking）。特点：表级锁：MyISAM使用表级锁来管理并发操作，这意味着在执行更新或删除操作时，整个表会被锁定，其他事务无法对表中的任何行进行操作。低并发性能：由于锁的粒度较大，多个事务不能同时对表中的不同行进行操作，从而降低了并发性能。不支持事务：MyISAM不支持事务，因此在需要事务支持的应用中不适用。读写分离：虽然MyISAM使用表级锁，但在读操作时，多个读操作可以同时进行，只有写操作会阻塞其他操作。锁的类型：读锁（Read Lock）：允许其他事务读取表中的数据，但不允许修改。写锁（Write Lock）：禁止其他事务读取或修改表中的数据。适用场景：以读操作为主的应用，如内容管理系统（CMS）。不需要事务支持的应用。3. 锁粒度对比特性InnoDB（行级锁）MyISAM（表级锁）锁粒度行级锁表级锁并发性能高并发性能，适合读写混合场景低并发性能，适合读多写少场景事务支持支持事务（ACID）不支持事务锁类型共享锁（S Lock）、排他锁（X Lock）读锁（Read Lock）、写锁（Write Lock）适用场景高并发读写操作、需要事务支持以读操作为主、不需要事务支持4. 示例InnoDB（行级锁）-- 创建InnoDB表 CREATE TABLE innodb_table ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), age INT ) ENGINE=InnoDB; -- 事务1 BEGIN; UPDATE innodb_table SET age = 26 WHERE id = 1; -- 事务2 BEGIN; UPDATE innodb_table SET age = 27 WHERE id = 2; -- 不会被事务1阻塞，因为锁的是不同行MyISAM（表级锁）-- 创建MyISAM表 CREATE TABLE myisam_table ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), age INT ) ENGINE=MyISAM; -- 事务1 LOCK TABLES myisam_table WRITE; UPDATE myisam_table SET age = 26 WHERE id = 1; -- 事务2 UPDATE myisam_table SET age = 27 WHERE id = 2; -- 会被事务1阻塞，因为整个表被锁定5. 总结InnoDB：适合高并发的读写操作。支持事务和行级锁，能够有效提高并发性能。适用于需要事务支持的应用，如金融系统、电子商务等。MyISAM：适合以读操作为主的应用。使用表级锁，适合读多写少的场景。不支持事务，适用于不需要事务支持的应用。在选择存储引擎时，需要根据应用的具体需求和场景来决定。如果需要高并发性能和事务支持，建议选择InnoDB；如果以读操作为主且不需要事务支持，可以选择MyISAM。

[Redis] string大数据量如何扩容
type: item
date: 2025-07-07T11:48:00+08:00
description: Redis string大数据量如何扩容Redis字符串（SDS）的扩容策略当字符串长度小于1MB时：如果当前SDS的剩余空间不足以容纳新数据，Redis会将SDS的容量扩展为当前长度的两倍。这种策略可以减少频繁的内存分配操作，提高性能。当字符串长度超过1MB时：每次扩容只会增加1MB的空间，而不是成倍扩展。这种策略可以避免一次性分配过多内存，导致资源浪费。最大长度限制：Redis字符串的最大长度为512MB。扩容过程当SDS的剩余空间不足以容纳新内容时，Redis会触发扩容操作：重新分配内存：根据上述策略计算新的内存分配大小，并分配新的内存空间。复制数据：将原有数据复制到新的内存空间中。释放旧内存：释放旧的内存空间。如果扩容导致SDS的类型发生变化（例如，从SDS_TYPE_8变为SDS_TYPE_16），Redis还会更新SDS的编码类型，并可能需要移动数据到新的内存位置。优化策略空间预分配：Redis通过预分配空间来减少内存分配的频率。当字符串长度小于1MB时，每次扩容会分配双倍空间；当长度超过1MB时，每次扩容增加1MB。惰性空间释放：当字符串缩短时，SDS不会立即释放多余的空间，而是保留这些空间以备后续使用，减少后续扩容的开销。这种扩容机制使得Redis能够在处理大量数据时保持高效的性能，同时避免了内存分配和释放带来的开销。和Go的切片扩容机制类似，但也只是形似。Go的是超过1024大小之后就以1.25倍扩容，redis的string是超过1mb就扩1mb，小于1024/1mb的时候就是扩当前长度的两倍。

[Golang/Redis] Redis使用Lua脚本示例三则
type: item
date: 2025-04-30T14:43:37+08:00
description: Redis使用Lua脚本示例三则一则：比较简易的使用Lua语言新增HSET的值package main import ( "context" "fmt" "log" "github.com/go-redis/redis/v8" ) var ctx = context.Background() func main() { // 连接到 Redis rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", // Redis 服务器地址 Password: "", // Redis 密码，没有则留空 DB: 0, // 默认数据库编号 }) // 测试连接 _, err := rdb.Ping(ctx).Result() if err != nil { log.Fatalf("Failed to connect to Redis: %v", err) } fmt.Println("Connected to Redis successfully!") // 定义 Lua 脚本 // 这个脚本会插入一个字段到 Hash 中，并返回该字段的值 luaScript := ` local key = KEYS[1] local field = KEYS[2] local value = ARGV[1] redis.call("HSET", key, field, value) return redis.call("HGET", key, field) ` // 调用 Lua 脚本 key := "myHash" field := "myField" value := "myValue" // 使用 EVAL 命令运行 Lua 脚本 result, err := rdb.Eval(ctx, luaScript, []string{key, field}, value).Result() if err != nil { log.Fatalf("Failed to execute Lua script: %v", err) } // 打印结果 fmt.Printf("Inserted and retrieved value: %v\n", result) }二则：将多个值新增进SET内package main import ( "context" "fmt" "github.com/go-redis/redis/v8" "log" ) //var ctx = context.Background() func main() { rdb := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", Password: "", DB: 0, //0就用默认的DB PoolSize: 100, //连接池的大小 }) _, err := rdb.Ping(context.Background()).Result() if err != nil { fmt.Printf("%s, redis连接失败\n", err.Error()) return } fmt.Printf("redis连接成功\n") // 定义 Lua 脚本 luaScript := ` for i = 1, #KEYS do redis.call("SET", KEYS[i], ARGV[i]) end local res = {} for i = 1, #KEYS do table.insert(res, redis.call("GET", KEYS[i])) end return res ` // 调用 Lua 脚本 keys := []string{"name", "email", "age"} values := []string{"leijie", "rolingg@qq.com", "22"} // 使用 EVAL 命令运行 Lua 脚本 results, err := rdb.Eval(context.Background(), luaScript, keys, values).Result() if err != nil { log.Fatalf("Failed to execute Lua script: %v", err) } fmt.Println(results) }三则：将多个值新增进HSETpackage main import ( "context" "fmt" "log" "github.com/go-redis/redis/v8" ) var ctx = context.Background() func main() { // …

[Golang] Map的底层实现原理与并发安全（待填坑）
type: item
date: 2024-10-20T17:47:00+08:00
description: Map的底层实现原理Go语言采用的是哈希查找表，并且使用链表法解决哈希冲突。另外在面试里面，一般问到Map都会问它是否是并发安全的。想要回答这类问题，只需要记住这一句话：Go语言提供的数据类型中，只有channel是并发安全的。Map自身并发安全失败场景func main() { //创建map m := make(map[int]string) var wg sync.waitGroup wg.Add(2) //协程1写 go func() { for true { m[0] = "xiaoming" } wg.Done() }() //协程2读 go func() for true { _ = m[0] } wg.Done() }() wg.wait() fmt . Print1n("结束") }编译后运行会报错：fatal error: concurrent map read and map write 原因笔记来源：详解Go语言如何解决map并发安全问题实现并发安全的Map读写锁+map只需要在map进行读操作的时候，加读取锁后再进行读取；添加、更新、删除、遍历、获取长度这些操作加写锁再进行操作。每次进行这些map相关的操作都要进行加锁，我们通过学习Go语言的锁就会知道，读锁可以实现并发读，但是写锁只能单个写，阻塞其他对于此处的读写操作，是独占的。因此这样虽然能够保证数据的安全性，但也会影响程序的运行效率。所以这种加读写锁的并发安全做法一般适用于读多写少的场景。分片+锁分片+锁，顾名思义就是将map分成多个片段，每个片段都由一个锁进行保护。数据通过散列函数将键分布到不同的小map片段中，从而减少锁的竞争，提高并发性能。这种方法适用于写操作较多，且写操作分布均匀的场景。分片map的原理是将map划分为多个片段，每个片段由一个单独的锁保护。当对map进行读写操作时，只需要锁定对应的map片段即可。这样可以大大提高并发读写的性能。sync.MapGo语言标准库提供的 sync.Map 类型，它不是为了替代普通的map，而是用于特定的场景。sync.Map 适用于读多写少的场景，并且写操作的key分布比较分散。sync.Map内部使用了一个只读的map和一个可写的map，读操作默认是无锁的，而写操作需要加锁。当 misses（未命中读map的次数）超过一定阈值时，会将可写的map复制到只读map中。sync.Map 不能被复制，且在高频率写操作时性能可能下降。sync.Map 是 Go 语言中提供的一个并发安全的 map 类型，它允许多个 goroutine 安全地使用 map 进行读写操作而不需要使用互斥锁（mutex）。sync.Map 包里有增删改查等 Go 官方封装好的方法，用起来很方便嗷！具体的话还请看我之前写的：[[Golang基础] Sync包](https://rolingg.top/index.php/archives/103/)读写锁+map 和 Sync.Map 相比较：读写锁+map优点：简单直观：使用sync.RWMutex 来保护一个普通的map，读操作使用读锁，写操作使用写锁。适合读多写少的场景：允许多个读操作并发执行，而写操作是独占的，适合读操作远多于写操作的情况。缺点：读操作性能受限：在高并发读操作下，虽然可以并发读取，但如果写操作频繁，读操作会因为锁的争用而受到性能影响。写操作独占：写操作需要独占锁，这会阻塞所有的读和写操作，可能导致性能瓶颈。sync.Map优点：并发性能高：sync.Map 通过读写分离和延迟删除机制，优化了读多写少的场景下的并发性能。空间换时间：使用两个map（read和dirty）来提高性能，减少了锁的使用，提高了并发读取的效率。延迟删除：删除操作不会立即清理内存，而是标记为删除，直到下次同步时才清理，减少了锁的操作。缺点：不适合大量写入的场景：在写操作频繁的场景下，sync.Map的性能可能下降，因为需要频繁地将dirty中的数据同步到read中。内存使用高：由于维护了两个map，可能会增加内存的使用。综上，sync.Map 对读操作加锁的优化更好，但是对于一些需要复制map，且对内存占用要求小的场景，用简单实现的读写锁+map会更好。总结写场景频繁，对性能要求也比较高的话，还是使用 分片+锁 的方式吧；其余的场景得看对内存的要求，对内存占用要求小的话，就用 读写锁+map，没这个需求的话，就用 sync.Map，方便快捷。

[Golang] Channel原理
type: item
date: 2024-10-20T15:46:00+08:00
description: Channel原理上来先贴一段源码type hchan struct { qcount uint // 队列中的总元素个数 dataqsiz uint // 环形队列大小，即可存放元素的个数 buf unsafe.Pointer // 环形队列指针 elemsize uint16 //每个元素的大小 closed uint32 //标识关闭状态 elemtype *_type // 元素类型 sendx uint // 发送索引，元素写入时存放到队列中的位置 recvx uint // 接收索引，元素从队列的该位置读出 recvq waitq // 等待读消息的goroutine队列 sendq waitq // 等待写消息的goroutine队列 lock mutex //互斥锁，chan不允许并发读写 } //recvq 那些已经准备好从 channel 中读取数据，但因为 channel 当前没有可供读取的数据而处于等待状态的 goroutines 的队列。 //sendq 等待机会将数据发送到 channel 中的 goroutine 的队列向 channel 写数据:若等待接收队列 recvq 不为空，则缓冲区中无数据或无缓冲区，将直接从 recvq 取出 G ，并把数据写入，最后把该 G 唤醒，结束发送过程。说明：如果接收队列 recvq 不为空，说明有 goroutine 正在等待从 channel 中读取数据。此时，如果 channel 的缓冲区中没有数据或者 channel 没有缓冲区，将直接从 recvq 中取出一个等待的 goroutine G，把要发送的数据写入 G，然后将 G 唤醒，结束发送过程。也就是说，原本写入的数据是写进 channel 的缓冲区中，但因为缓冲区没有数据或者channel没有缓冲区，导致这时，发送数据的操作不是将数据放入缓冲区，而是直接将数据传递给这个等待读取的 goroutine G。有人可能会有疑问，为什么channel有缓冲区，而且缓冲区没有数据，写入的数据不写入缓冲区而是写入等待读消息的goroutine G内。这是因为有等待读消息的goroutine G要的就是缓冲区的数据，但是由于缓冲区没有数据了，又有新数据来，不如直接将 数据 → 等待读消息的goroutine G，而不是多一步的 数据 → channel缓冲区 → 等待读消息的goroutine G。这样做①是可以提高效率，直接将数据传递给等待读取的 goroutine 可以减少数据复制的开销，因为数据不需要先写入缓冲区再从缓冲区读取出来。这样可以提高程序的效率。②是可以避免死锁，如果缓冲区为空，而发送的数据被写入缓冲区，那么在没有其他 goroutine 准备读取的情况下，发送操作将无法完成，这可能导致死锁。通过直接将数据传递给等待读取的 goroutine，可以避免这种情况。若等待接收队列 recvq 为空，且缓冲区中有空余位置，则将数据写入缓冲区，结束发送过程。若等待接收队列 recvq 不为空，且缓冲区中没有空余位置，则将发送数据写入 G，将当前 G 加入 sendq ，进入睡眠，等待被读 goroutine 唤醒。从 channel 读数据：若等待发送队列 sendq 不为空，且没有缓冲区，直接从 sendq 中取出 G ，把 G 中数据读出，最后把 G 唤醒，结束读取过程。说明：当从无缓冲的 channel 读取数据时，如果发送队列 sendq 不为空，这意味着有 goroutine 正在等待发送数据到这个变量。无缓冲的 channel 要求发送和接收操作同步进行，即发送方必须等待接收方准备好接收数据。因为 channel 是无缓冲的，所以数据是在等待发送的 goroutine G 的本地变量中。因为数据已经被接收方读取（也就是channel读取了）。G 可以继续执行它之后的代码，通常是在 G 中的 send 操作之后的代码。在有缓冲区的情况下，若等待发送队列 sendq 不为空，说明缓冲区已满，从缓冲区中首部读出数据，把 G 中数据写入缓冲区尾部，把 G 唤醒，结束读取过程。此情况基于有一个 goroutine 尝试从 channel 读取数据。如果缓冲区中有数据，则从缓冲区取出数据（指有一个 goroutine 尝试从 channel 读取数据），结束读取过程。这个操作不需要考虑 sendq，因为缓冲区中已经有数据可以被读取。图同2差不多将当前 goroutine 加入 recvq ，进入睡眠，等待被写 goroutine 唤醒。在这种情况下，因为没有数据可读且没有发送操作在等待（也就是没有缓冲区或者缓冲区内没有数据，同时 sendq 为空），执行读取操作的 goroutine 将自身加入到与 c…

[Kafka] 高可用实例
type: item
date: 2024-09-29T14:44:19+08:00
description: Kafka高可用实例kafka 建议不要用 单分区的Topic 去进行消息的持久化存储，这要会导致一些问题。例如：一个集群原本有三个节点，但是只用了一个节点。Kafka集群中一个节点出问题了，而有需要用到的服务对应的 Topic 单分区在这个节点上，服务所有的副本都部署在这个节点上，就会导致服务的功能异常，整个功能不可用。这种叫 单点故障，不满足高可用。解决方法也很简单，调整 Topic 的分区数量即可。调整 Topic 的分区数量，需要分区的副本数量和 kafka 集群节点数量保持一致，确保当 kafka 其中一个节点出现问题时，对应的服务功能能够正常工作。这个问题，Topic 分区对应的服务全都在一个节点内，其他节点没有对应的 Topic 分区时，当这个节点出现问题，就会导致整的服务挂掉。但是如果多分区，就能让问题服务能够正常运行，这其实归于Kafka的分区策略、负载均衡策略和故障转移策略。当从 1 个分区增加到 3 个分区时，增加分区数量，这样可以分散负载，提高并行处理能力。一般来说，还是会手动将 3 个分区分配到 3 个broker中，一个broker内分别有3个分区及他们的副本，保证可用性，也应证了这句话 需要分区的副本数量和 kafka 集群节点数量保持一致。这样做之后，一个Topic就在多个节点上有多个分区，即便一个节点挂了，其他分区也有对应的分区及副本，保证可用性这样当有这个 Topic 分区的节点出问题时，上面的服务所在的分区就会自行进行新的Leader选举，因为其他的节点有可用分区及副本，及保证了的高服务可用。总结不要那么傻傻的一个Topic里只有一个节点挂载着服务的分区和副本，要保证可用性，最好就是多节点都有对应的服务分区和副本。记得别忘了把消费者上涨到上调到 Topic 分区数+1或以上个的数量，这是为了保证每个分区的消息队列都有消费者进行消费，即便消费者挂掉了一个，也有新的消费者填上，一致保持着良性的消费过程，不让消息堆积在消息队列中。

[Kubernetes] 调度策略及相关内容
type: item
date: 2024-09-20T22:00:00+08:00
description: Kubernetes调度策略及相关内容Scheduler调度的过程和策略事先补充：Node 是 Kubernetes 集群中的工作节点一个 Node 可以运行多个 Pod，而一个 Pod 只能运行在一个 Node 上使用标签和选择器可以管理 Node 和 Pod 之间的关系，从而实现灵活的调度和管理。Scheduler 是 Kubernetes 集群的调度器，把 Pod 分配给集群的节点（Node）scheduler 是一个单独运行的程序，只要启动之后就会一直监听apiserver。获取报文中的字段（即spec中的nodeName字段）创建 Pod 时，为每个 Pod 创建一个 binding，表示该往哪个节点上部署。调度规则：公平——每个节点都能够分配资源资源高效利用——集群中的资源可以被最大化使用效率——调度的性能要好，能够尽快的完成大批量 Pod 的调度工作灵活——允许用户根据自己的需求，控制和改变调度的逻辑kubernetes对Node的选择策略预算策略：用于过滤出合适的节点优选策略：用于选择出合适的部署节点选择 Node 的过程：先执行预算策略，再执行优先策略。这两步的操作都必须成功，否则立刻返回报错。以上都是自带的算法策略，由K8s集群自己来选择。预算策略podfitsresources：Pod 的适应策源，检查节点上剩余的资源是否满足pod请求的资源（主要是CPU和内存）podfitshost：Pod 适应主机，如果Pod指定了 Node 的 name，检测主机名是否存在，如果存在要和 Pod 指定的名称匹配，这才能调度过去podselectormarches：Pod 选择器匹配，根据选择器选择的 Node 标签进行匹配，如果 Node 上存在匹配的标签，则选择该 Node。nodeskconflict：无磁盘冲突，确保已经挂载的卷和Pod的卷不发生冲突，除非目录是只读。如果预算策略不满足，Pod 将始终处于pending状态，不断的重试调度，直到节点满足条件为止。优选策略leastrequestedpriority：最低请求优先级，通过算法计算节点上的CPU和内存使用率，确定节点的权重。使用率越低的节点，相应的权重就越高。调度时会更倾向于这些使用率低的节点。实现资源合理的利用。越低，被使用的资源越少，则被调用的权重越高。balancereasourceallocation：平衡资源分配，算CPU和内存的使用率，给节点赋予权重。权重算的是CPU和内存使用率接近，权重越高。和上面的最低请求优先级一起使用。使用率越接近，两种资源的量越接近，因为其中一个资源不够用的概率越小，被调用的权重越高。例如：Node① CPU：20%，内存：80%Node② CPU：50%，内存：50%如果选择Node①，则因为内存不足的可能极大，导致使用出错；如果使用Node②，两个资源剩余的量都很平均，出现其中一个资源不足导致使用出错的问题概率变小；主打一个要么都够用，要么都不够用。imagelocalitypriority：节点上是否已经有了要部署的镜像。镜像的总数成正比，满足的镜像数越多，权重越高。已经有的镜像越多，则要做的准备工作就越少（因为未部署的镜像要拉取配置啥的）。满足的镜像越多部署的就越快。kubernetes对Pod的调度策略在 Kubernetes 中，调度是指将 Pod 放置到合适的节点上，以便对应节点上的 Kubelet 能够运行这些 Pod。Kubelet 是 Kubernetes 集群中的一个关键组件，它是集群中的每个节点（Node）上运行的代理服务。Kubelet 的主要作用是确保容器都运行在 Pod 中，并且这些 Pod 符合集群的状态。它负责维护容器的生命周期，包括创建、更新和销毁容器。定向调度：使用 nodeName 字段指定 Node 名称；使用 nodeSelector 字段指定 Node 标签。亲和性调度：使用 Node/Pod 亲和性（NodeAffinity、PodAffinity、PodAntiAffinity）；污点与容忍：使用 Node 设置污点，结合 Pod 设置容忍。全自动调度：运行在哪个节点上完全由 Scheduler 经过一系列的算法计算得出。定向调度nodeName：指定节点对应的节点名称，用于将 Pod 调度到指定的 Node 上，不经过调度器。apiVersion: v1 kind: Pod metadata: name: my-pod spec: containers: - name: my-container image: my-image nodeName: "node-name"在参数中设置了nodeName，指定了节点的名称。这个规则是强制匹配,会跳过schedule…

[面试] 腾讯csig运维实习生面经
type: item
date: 2024-06-20T21:47:00+08:00
description: 腾讯csig运维实习生面经面试岗位：运维实习生 150~200一天 腾讯，针不戳先说体验吧，整体感觉真的很好，面试官老哥人好，沟通也很流畅，问问题讲的也清楚，生怕你没听懂还可以多讲几遍给你听。面试主要问的Go语言的内容比较多，或者说问的都是Go相关的，前面问的都挺简单的：①常量与变量的区别②GC回收（老生常谈）③反射与断言（这个我没答出来，我拉了，用少了也看少了）但是后面叫我根据场景写需求的时候我就懵了，咱第一次被这样面，咱现在代码水平也比较拉胯，所以铁寄了：场景：爬虫爬网站，用channel去接收与传出结果，要求爬失败了的网站该怎么用goroutine去解决。这个场景其实挺简单的，但奈何自己太久没用channel和goroutine，没怎么写过相应的代码，拉垮了，写不出一点。之后就是聊天环节了，老哥也是一眼发现了我的问题，让我去好好地把这些基础打好，然后看看k8s的源码或者大佬的代码，去理解他们代码的设计与思路，之后就可以尝试一下二次开发。总结：这次虽然好好地介绍了自己，但因为自己基础能力综合运用真的不行，Golang特性也没有好好学好。通过面试官老哥的指点也让我明白了之后的方向该怎么走，至于算法，连写的机会都没有，更得提升。这次面试体验真的很好，算是遇到了贵人指点，过程老哥也知道我自学Go方向有点差错，指点了我。我估计这辈子遇不到多几个这样的贵人了。希望之后沉淀完能真的达到老哥说的境界，这样对自己也好，对之后自己拿对应岗位的薪资也有支柱能撑得起自己。最后附上面试时候场景题的解答，确实不难：package main import ( "fmt" "net/http" "sync" "time" ) var waitGroup sync.WaitGroup func main() { ch := make(chan string) waitGroup.Add(1) //开一个协程 go func() { defer waitGroup.Done() res, err := http.Get("https://rolingg.top") if err != nil { ch <- fmt.Sprintf("爬虫爬网站出错, err: %s", err.Error()) } else { defer res.Body.Close() // 确保关闭响应体 ch <- fmt.Sprintf("爬虫爬网站成功！status: %s\n", res.Status) } }() //注意了，这里default获取结果要比case快的，因为协程中运行，肯定要比主线程慢一点，而且channel赋值要时间，就那一丢丢时间就足以让default先符合条件触发 //select { //case result := <-ch: // fmt.Println(result) //case <-time.After(3 * time.Second): // fmt.Println("爬虫爬取网站超时") // return //default: // fmt.Println("奇奇怪怪") //} //注意，select在获得到结果之前也会阻塞，直至有一个条件符合才会往下走 select { case result := <-ch: fmt.Println(result) case <-time.After(3 * time.Second): fmt.Println("爬虫爬取网站超时") return } close(ch) }