最近感觉面试时容易语无伦次,还有就是容易扯些有的没的抓不住重点,需要改掉这个坏毛病。

首先是面试不要紧张,当然人在不知道怎么回答的是时候是肯定会紧张的,这方面还要做练习。

查看 sudo 权限用户

# Get entries from administrative database.
getent group sudo
# or check config file
cat /etc/sudoers

k8s 中暴露 pod 不同方式的区别

下面 3 种方式都可以暴露 pod,区别依次是:给单个 node 上的 pod 提供负载均衡;给一个 svc 匹配的 deploy 提供负载均衡;给一组 svc 提供负载均衡。

  • nodeport svc –> 通过 node nat 的方式,将对 node 端口的访问映射到 pod 端口,node ip 地址到 node 里的 pods,4 层负载均衡。
  • lb svc –> svc 指向为外部的一个 lb 的 domain,让这个 lb 代理和 svc 匹配的 deploy 的流量,给 svc 对应的 deploy 提供 7 层负载均衡。
  • external lb + nodeport svc –> 配置外部的 lb 映射(ingress)集群里的 node port svc,给多个 svc 提供负载均衡。

对称加密和非对称加密

对称加密

  • 双方使用相同的密钥进行加密和解密。
  • 优点:加密解密速度快,适合大数据量的加密。
  • 缺点:密钥分发安全性难以保障,一旦密钥泄露,通信内容可能被破解。
  • 常见算法:AES、DES、3DES、Blowfish。

非对称加密

  • 使用一对密钥:公钥和私钥。公钥用来加密,私钥用来解密,反之亦可。
  • 优点:不需要共享私钥,公钥可以公开,安全性更高。
  • 缺点:加密解密速度较慢,适合少量数据或密钥交换。
  • 常见算法:RSA、ECC、DSA。

LB load Off SSL,证书存储在哪里

LB 如何 Load Off SSL

  • SSL 卸载 (SSL Offloading): LB (Load Balancer) 负责终止客户端的 SSL/TLS 连接,解密流量,然后将明文流量转发给后端服务器。这种方式可以减轻后端服务器的计算负担,提高性能。
  • 工作原理: 客户端与 LB 建立 SSL/TLS 连接,发送加密流量。 LB 使用存储的证书和私钥对流量进行解密。 解密后的明文流量转发给后端服务器

LB 的证书和 Key 存储

  • 存储位置
    1. LB 配置文件:证书和私钥通常存储在 LB 的文件系统中,常见路径如 /etc/ssl/ 或指定的配置目录。
    2. 证书管理系统:一些 LB 支持集成外部证书管理工具或自动获取证书(如 Let’s Encrypt)。
    3. 硬件安全模块 (HSM):在高安全需求场景中,证书和私钥可能存储在专用 HSM 中,避免泄露。
  • 配置方式: Nginx:通过 ssl_certificatessl_certificate_key 配置证书和密钥路径。 AWS ALB:上传证书到 AWS Certificate Manager (ACM) 或直接绑定证书到 LB。 HAProxy:通过配置文件引用证书和密钥路径。

使用不同的 tf state file

为什么分离 State File

  1. 模块化管理:将资源(如网络、数据库、应用程序)分开管理,每类资源有独立的 state file。
  2. 避免冲突:多个团队或 pipeline 并行操作时,减少对同一 state file 的竞争。
  3. 精细化权限控制:为不同资源分配不同的管理权限。

方式 1. 定义多个 Terraform 工作目录 每类资源单独定义一个工作目录,并在每个目录中维护独立的 main.tfstate file

├── network/
│   ├── main.tf
│   ├── terraform.tfstate
├── database/
│   ├── main.tf
│   ├── terraform.tfstate
├── app/
	├── main.tf
	├── terraform.tfstate

方式 2. 使用 backend 配置不同的 State 通过为每个模块指定不同的 backend 配置,确保 state 文件隔离。例如:

// network/main.tf
terraform {
  backend "s3" {
	bucket         = "my-terraform-states"
	key            = "network/terraform.tfstate"
	region         = "us-east-1"
  }
}
// database/main.tf
terraform {
  backend "s3" {
	bucket         = "my-terraform-states"
	key            = "database/terraform.tfstate"
	region         = "us-east-1"
  }
}

方式 3. 通过命令行指定 State File 如果所有配置在同一个目录下,可以通过 -state 参数指定使用哪个 state file:

terraform plan -state="terraform-network.tfstate"
terraform apply -state="terraform-network.tfstate"

方式 4. 使用工作区 (Workspaces) Terraform 提供工作区机制,用于支持同一配置的多环境管理(如 dev, prod)。

terraform workspace new dev
terraform workspace select dev
terraform plan
terraform apply

分层设计 通过 terraform_remote_state 数据源共享不同 state 文件中的输出变量。例如,network 的输出可被 app 使用:

data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
	bucket = "my-terraform-states"
	key    = "network/terraform.tfstate"
	region = "us-east-1"
  }
}

resource "aws_instance" "example" {
  subnet_id = data.terraform_remote_state.network.outputs.subnet_id
}

注意事项

  1. State File 存储位置:推荐使用远程后端(如 S3、GCS)来存储 state file,避免本地存储导致管理混乱或丢失。
  2. 明确依赖关系:确保模块间的依赖通过 outputsterraform_remote_state 明确传递,避免隐式耦合。
  3. 锁机制:使用远程后端自带的锁机制(如 S3 + DynamoDB),避免并发冲突。

父 Shell 如何传递变量给子 Shell

方法 1: 使用 export 导出变量 在父 Shell 中通过 export 命令将变量声明为环境变量,子 Shell 就能继承并访问。

# 父 Shell
export VAR="Hello, World!"
bash # 进入子 Shell

# 子 Shell
echo $VAR  # 输出: Hello, World!

方法 2: 直接在调用子 Shell 时传递变量

可以在调用子 Shell 时直接通过命令行传递变量:

VAR="Hello, Subshell!" bash -c 'echo $VAR'

方法 3: 使用脚本文件传递变量 将变量写入脚本,子 Shell 通过执行脚本继承变量。 脚本文件 set_vars.sh

export VAR="Script Variable"

父 Shell:

source set_vars.sh  # 加载脚本变量到父 Shell 环境
bash # 进入子 Shell

# 子 Shell
echo $VAR  # 输出: Script Variable

注意事项

  1. 未导出的变量不可继承:未通过 export 声明的变量是局部变量,仅在当前 Shell 有效,子 Shell 无法访问。
  2. 子 Shell 修改不影响父 Shell:子 Shell 对变量的修改不会传递回父 Shell。
    export VAR="Parent"
    bash -c 'VAR="Child"; echo $VAR'  # 子 Shell 输出: Child
    echo $VAR  # 父 Shell 输出: Parent
    
  3. 尽量避免污染环境:在大型脚本中,应使用局部变量或明确清理环境变量,防止变量混乱。

L2 和 L3 网络隔离对比

L2 层(交换机层)网络隔离 通过 VLAN(如 XVLAN、802.1Q VLAN)实现网络隔离。 优点:

  1. 性能高:隔离发生在数据链路层,数据包无需经过路由器,转发延迟低。
  2. 隔离性强:L2 隔离直接限制广播域和数据链路层通信,安全性高。
  3. 带宽利用率高:数据包在 L2 交换机内处理,无需占用 L3 路由器的资源。
  4. 成本较低:多数 L2 交换机支持 VLAN,无需额外设备或复杂配置。 缺点:
  5. 灵活性较低:VLAN 的隔离范围通常受交换机拓扑限制,跨交换机部署复杂(需要支持 VLAN Trunk)。
  6. 可扩展性差:VLAN 数量有限(传统 VLAN 支持 4096 个 ID),在大规模网络中可能不足。
  7. 难以跨区域隔离:L2 隔离依赖物理拓扑,无法轻松实现跨数据中心或远距离隔离。

L3 层(路由器层)网络隔离 通过不同的 IP 网络或子网来实现隔离,例如使用不同的子网段或路由策略。 优点:

  1. 灵活性高:L3 隔离与物理拓扑无关,可通过 IP 地址和路由策略灵活配置。
  2. 可扩展性强:IP 地址空间非常大,支持任意规模的隔离需求。
  3. 支持跨区域隔离:L3 隔离可以轻松实现跨数据中心或远距离的网络隔离。
  4. 可与安全策略结合:结合防火墙或 ACL(访问控制列表)可实现更精细的流量控制。 缺点:
  5. 性能较低:L3 隔离依赖路由器处理流量,路由性能可能成为瓶颈。
  6. 广播流量限制:L3 不支持 L2 的广播,某些协议(如 ARP)在隔离环境中无法正常工作。
  7. 复杂性较高:配置路由表和网络策略更复杂,尤其是动态路由协议的引入会增加管理难度。

总结

  • L2 隔离适用场景: 高性能、局域范围内的隔离,如小型网络、同数据中心内的隔离需求。
  • L3 隔离适用场景: 灵活性和跨区域要求高的场景,如多数据中心、多租户云服务环境。

容器网络和虚拟机网络的区别

容器网络

  1. 通过 Linux Namespace、CNI(容器网络接口)等技术实现。
  2. 轻量:基于 Linux 内核直接实现,不需要额外的硬件虚拟化层。
  3. 启动速度快:容器启动时直接创建网络命名空间和虚拟网卡,几乎无延迟。
  4. 隔离性较弱:容器之间共用主机内核,网络隔离依赖命名空间和额外配置(如 iptables)。
  5. 常见实现: Bridge 网络:容器通过虚拟网桥连接主机。 Host 网络:容器直接使用主机的网络栈,性能高但无隔离。 Overlay 网络:通过 VXLAN 等技术支持跨主机容器通信,灵活但性能稍差。

虚拟机网络

  • 通过虚拟化平台(如 KVM、VMware)和虚拟网卡(vNIC)实现。
  • 较重:需要虚拟化层和完整的操作系统,资源开销较大。
  • 启动速度慢:网络初始化包括虚拟网卡、虚拟交换机等,启动时间比容器长。
  • 隔离性强:每个虚拟机都有独立的网络栈,与主机和其他虚拟机完全隔离。
  • 常见实现: Bridge 网络:虚拟机通过虚拟网桥接入物理网络。 NAT 网络:虚拟机通过主机的 NAT 出访外部网络,适合小规模实验。 SR-IOV:直接分配物理网卡的虚拟功能给虚拟机,性能接近裸机。

总结

  • 容器网络适用场景: 快速部署、轻量级应用和需要高密度部署的场景(如微服务架构)。
  • 虚拟机网络适用场景: 对安全隔离、性能或完整操作系统支持要求高的场景(如多租户云环境)。

用户数据库设计

一个数据库(mysql)里有多个库(db),一个 db 里有多个表(table),数据量不大的时候一张表就能搞定,但是当用户数据量大的时候就要分库分表,这里通用的做法是

用户网络设计

How should I be supposed to answer this interview question? : r/networking

确认用户需求 与用户沟通,明确他们的业务需求和目标:

  • 环境划分:是否需要区分开发(dev)、测试(stg)、生产(prod)等网络?
  • 网络隔离:这些环境之间是否需要完全隔离?还是需要特定规则下的通信?
  • 用户规模:需要支持多少用户和设备?未来是否会扩展?
  • 网络流量:流量模式如何(南北向流量 vs 东西向流量)?是否有高带宽需求?
  • 安全性:是否需要网络分段(segmentation)、防火墙、VPN 访问等?
  • 应用类型:是否涉及数据库、负载均衡、容器化应用等特殊需求?

初步设计拓扑 基于需求,设计合理的网络结构:

  • 多环境划分: 如果 dev、stg、prod 需要隔离,建议使用不同的网络(VLAN 或 VXLAN)。 如果需要部分通信,可以通过路由或防火墙规则实现受控访问。
  • 选择网络类型: L2 网络:**适用于小型网络,直接用 VLAN 实现隔离,简单高效。 L3 网络:适用于大型网络,通过子网划分、路由器或 SDN 实现隔离,灵活可扩展。
  • 规划 IP 地址: 使用 CIDR 规划 IP 段,比如: dev:192.168.10.0/24 stg:192.168.20.0/24 prod:192.168.30.0/24
  • 负载均衡和高可用性:是否需要配置负载均衡器或冗余链路(如 LACP)。

考虑安全和扩展

  • 安全策略: 使用 ACL、防火墙和隔离策略保护不同网络。 关键服务放在 DMZ(如公共接口服务)。 配置 DDoS 防护和 IDS/IPS。
  • 扩展性: 为未来预留足够的 IP 地址空间。 考虑云平台或混合架构,是否需要对接 AWS、Azure 等。

部署和管理

  • 配置路由器、交换机、防火墙,使用管理工具(如 Ansible)自动化配置。
  • 使用监控工具(如 Zabbix、Prometheus)实时监控网络运行状态。

回答示例: " 首先,我会与用户确认需求,比如是否需要开发、测试、生产环境的划分,这些网络之间是否需要完全隔离,未来是否需要扩展等。如果需要隔离,我会考虑用 VLAN 或 L3 网络划分不同的环境,并规划合理的 IP 地址段(如 dev 是 192.168.10.0/24)。在安全上,我会配置防火墙和访问控制规则,并为关键服务划分 DMZ。如果网络规模较大,还可以使用 L3 路由或 SDN 提升管理效率和灵活性,同时确保预留扩展空间。"

pod 生命周期

Pod 的生命周期 | Kubernetes

github action job 和 step

Job 是 GitHub Actions 中执行的一个独立任务,它包含一组步骤(Steps),并运行在指定的 Runner 环境(如 ubuntu-latestwindows-latest)。

  1. 独立运行环境: 每个 Job 在自己的虚拟机或容器中运行,互不干扰。
  2. 并行/依赖运行: 多个 Job 可以并行运行,或者通过 needs 指定依赖关系。
  3. 状态隔离: 不同 Jobs 之间的文件系统、环境变量等默认是独立的,除非通过 artifactcache 来共享数据。

Step 是 Job 中的最小执行单位,每个 Step 执行一条命令或操作。

  1. 顺序执行: Steps 在 Job 内按定义的顺序依次执行。
  2. 共享上下文: 同一个 Job 中的 Steps 共享运行环境,可以访问相同的文件系统和环境变量。
  3. 复用 Action: Step 可以调用 GitHub Actions 官方或自定义的 Actions,也可以直接运行 Shell 命令。

Job: 用于划分大的任务,比如构建、测试、部署等,可以并行或串行运行。 Step: 用于定义具体的操作步骤,如安装依赖、运行命令、调用自定义 Action 等。

如果在同一个 GitHub Actions 工作流(workflow)文件中定义了多个 Job,并且这些 Job 之间没有依赖关系(即没有通过 needs 进行显式指定依赖),那么它们会 并发 执行。s

  • 并行运行: 默认情况下,GitHub Actions 会并行运行独立的 Job。
  • 依赖关系: 如果某个 Job 依赖于其他 Job 的完成,那么该 Job 会在依赖的 Job 完成后才开始执行。通过 needs 来设置依赖关系。

加载 linux 内核模块

可以通过 /boot 目录中的配置文件或 zcat /proc/config.gz 来查看当前系统的内核配置。

修改内核配置

# 配置内核
cd /usr/src/linux
make menuconfig

# 编译和安装新的内核
make -j$(nproc)
make modules_install
make install

# 重启系统后选择新内核

加载内核模块

某些内核功能是通过内核模块来实现的。如果你需要启用某些功能(如网络协议、文件系统或硬件支持),通常是通过加载相应的内核模块。可以使用 modprobeinsmod 来加载这些模块。

# 加载模块
sudo modprobe ipv6

# 查看已加载的模块
lsmod

# 使模块在启动时自动加载
echo "ipv6" | sudo tee -a /etc/modules-load.d/ipv6.conf

修改内核参数(sysctl)

Linux 内核的许多功能可以通过修改内核参数来启用或禁用。你可以使用 sysctl 命令来查看和修改这些参数。

# 查看当前的内核参数
sysctl -a

# 临时修改内核参数,启用 IP 转发
sudo sysctl -w net.ipv4.ip_forward=1

# 永久修改内核参数,使修改在重启后生效
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

优化 cicd 流水线速度

并行化作业和步骤

  • 并行执行:如果工作流中有多个步骤或作业可以并行执行(例如,独立的测试任务或构建任务),可以将它们拆分成并行作业。大多数 CI/CD 工具(如 GitHub Actions、GitLab CI)都支持并行化作业。
  • 工作流分割:将大的工作流拆分成多个小的工作流,避免不必要的任务串行执行。

优化构建和测试过程

  • 缓存依赖:缓存依赖项或构建产物(例如,依赖包、构建文件夹),避免每次都重新下载或重新编译。大部分 CI/CD 工具支持缓存机制(例如,GitHub Actions 的 actions/cache)。
  • 增量构建:采用增量构建,只构建变动部分,避免全量构建,尤其是对于大型项目。
  • 并行测试:通过将测试分为多个阶段或进程来并行运行测试,减少单个测试过程的耗时。例如,可以使用工具如 pytest-xdist(Python)来并行执行测试。

优化容器和虚拟化资源

  • Docker 镜像优化:在使用容器时,确保镜像尽量小,减少构建和拉取镜像的时间。使用多阶段构建来分离构建和生产环境。
  • 使用预构建镜像:如果是常用的开发环境或依赖,可以考虑使用预构建镜像,而不是每次都重新构建。

减少不必要的步骤

  • 删除无用步骤:检查并移除 CI/CD 流程中不再需要的步骤或检查点(例如,某些无效的构建步骤,重复的测试等)。
  • 精简工作流:如果某些步骤对部署没有直接影响(例如,静态检查、代码风格检查),可以将它们移到独立的工作流中,避免影响主工作流。

优化资源和硬件配置

  • 提高资源配置:确保 CI/CD 系统的资源配置(如 CPU、内存、磁盘 I/O)足够高效,适当增加硬件配置或使用更快的构建机器。
  • 分布式 CI/CD:对于复杂的项目,可以使用分布式 CI/CD 架构,通过分配多个代理来提高整体效率。

避免重复执行

  • 条件触发:设置适当的触发条件,避免不必要的构建。例如,只在特定的分支或文件变化时触发 CI/CD 流程,避免每次提交都触发完整的工作流。
  • 推迟不重要的任务:例如,部署任务可以推迟到工作日的空闲时间,或者在低流量期间进行。

监控和分析性能瓶颈

  • 日志分析:查看工作流的执行日志,找出哪些步骤消耗时间最多,特别是构建、测试和部署阶段。
  • 性能优化工具:使用 CI/CD 工具自带的性能报告,或者第三方分析工具来识别瓶颈。
  • 逐步调优:针对具体的瓶颈进行细化优化,比如优化编译选项、减少 I/O 操作、优化依赖管理等。

程序在服务器上的结果不如预期时的调试

当开发的程序在本机运行正确,但在 Linux 服务器上出现问题时,首先确认环境差异(os 版本,依赖和库,环境变量),权限和用户,如果没有问题则确认本机和服务器上使用的是相同的编译和构建命令,最后是用一些调试工具。

  • gdb:如果程序崩溃或发生段错误(segmentation fault),可以使用 gdb 来调试程序,查看崩溃的堆栈信息,也可通过这个来断点调试。
  • strace:使用 strace 跟踪系统调用,检查程序在服务器上执行时的行为,特别是文件读写、网络请求等操作。
  • lsof:使用 lsof 查看程序打开的文件和网络连接,确认文件是否能正确打开或是否有其他进程占用。

gradle 如何禁止并发构建

1. 使用 org.gradle.parallel 属性

# 可以在 `gradle.properties` 文件中设置 `org.gradle.parallel` 为 `false`
org.gradle.parallel=false

2. 配置 --no-parallel 命令行选项

gradle build --no-parallel

这将禁止构建过程中任务的并行执行。

3. 限制构建并发数 如果只是想控制并发的任务数,而不完全禁止并行执行,可以在 gradle.properties 文件中设置 org.gradle.workers.max 来限制并发任务的最大数量:

org.gradle.workers.max=1

这将把最大并发任务数限制为 1,达到禁止并发构建的效果。

4. 通过锁定机制 如果需要在不同的构建之间进行互斥,可以使用 Gradle 的 锁定机制 来确保多个构建不会同时运行。例如,可以使用 buildLock 插件或者使用 Task 来创建一个锁,保证在同一时刻只有一个构建进程。

oom killer 回收其他进程来确保重要进程

方法 1: 使用 oom_score_adj 调整进程优先级

每个进程都有一个 oom_score,表示该进程在内存不足时被终止的优先级。默认情况下,OOM Killer 会根据这个 oom_score 来选择进程终止。值越高,表示进程越容易被杀死。通过调整进程的 oom_score_adj,可以影响 OOM Killer 的决策,降低进程被杀死的可能性。

oom_score_adj 的值范围

  • -1000:完全避免被 OOM Killer 选择。
  • 0:默认值,表示正常的优先级。
  • 1000:表示该进程非常容易被 OOM Killer 选择。
cat /proc/<pid>/oom_score_adj

# 确保进程的内存不容易被 OOM Killer 回收
echo -1000 > /proc/1234/oom_score_adj

方法 2: 调整 vm.overcommit_memoryvm.overcommit_ratio

在某些情况下,OOM Killer 会在内存接近不足时被触发。你可以通过调整系统的内存提交策略,来改变内存分配的行为。

vm.overcommit_memory:控制内存分配的策略。

  • 0:内核根据 heuristics(启发式)来决定是否允许内存分配。
  • 1:允许总是进行内存分配,不会检查内存是否足够。
  • 2:内核会在内存不足时阻止分配。 vm.overcommit_ratio:控制内存分配时最大内存使用比例。
sysctl -w vm.overcommit_memory=2
sysctl -w vm.overcommit_ratio=80

cpu 利用率不高但系统 load 很高如何排查

首先要搞清楚 top 命令的结果中的 load 是怎么算出来的。

# 三个数字分别代表了1分钟,5分钟,15分钟的统计值
load average: 0.42, 0.50, 0.51
  • 首先是这个 load 的值是从运行中的进程的角度来计算的
  • 每隔 5 秒钟,采样当前系统中运行队列(CPU 正在执行的进程 + 等待 CPU 的进程 + 等待 I/O 的进程)的长度。
  • 理解这里的长度可以简单理解为 5 秒钟的 cpu 时间长度,在这个时间长度内进程有没有被塞满,在单核系统中,如果是 1 的话则表示系统利用率 100%,大于 1 表示有线程在等待 cpu,小于 1 则是空闲。
  • 在多核系统中,时间长度相当于翻倍,所以在 4 核 CPU 的系统中,Load Average = 4.0 表示系统运行在满负载

如果 CPU 利用率不高系统负载(Load Average)很高,这通常意味着大量线程处于等待状态。

  • I/O 瓶颈:磁盘或者网络
  • 高内存占用或内存不足:系统频繁发生内存换页(swapping),线程因缺页中断而等待内存
  • 锁竞争(线程互斥锁、读写锁)
  • 僵尸进程或资源泄露
  • 内核问题或驱动问题
echo "===================== 系统总体状态 (top) ====================="
top -bn1 | head -n 20

echo -e "\n===================== 磁盘 I/O 状态 (iostat) ====================="
iostat -x 1 3

echo -e "\n===================== 磁盘 I/O 详情 (iotop) ====================="
sudo iotop -b -n 5 | head -n 20

echo -e "\n===================== 网络 I/O 状态 (iftop) ====================="
sudo iftop

echo -e "\n===================== 锁竞争和上下文切换 (pidstat) ====================="
pidstat -w 1 5

echo -e "\n===================== 等待状态进程 (ps) ====================="
ps -eo pid,ppid,cmd,%mem,%cpu,stat | grep '^D'

echo -e "\n===================== 内存使用情况 (vmstat) ====================="
vmstat 1 5

echo -e "\n===================== 堆栈分析 (strace 示例) ====================="
echo "找到处于 'D' 状态的进程并使用 strace 分析其系统调用。"
echo "例如:sudo strace -p <PID>"

echo -e "\n===================== 内核日志 (dmesg) ====================="
dmesg | tail -n 20

gke 和 eks 以及 k8s 降低成本

在 GKE(Google Kubernetes Engine)和 EKS(Amazon Elastic Kubernetes Service)中,提供了多个工具和服务,可以帮助客户降低 Kubernetes 集群的成本。以下是一些关键的工具和策略:

1. GKE(Google Kubernetes Engine)

GKE Autopilot

  • 功能:GKE Autopilot 是一种全托管的 Kubernetes 模式,Google Cloud 负责管理所有节点的基础设施。它帮助用户减少不必要的资源浪费,自动进行节点规模扩展和自动优化集群配置。
  • 节省成本的方式:无需手动配置和管理节点,自动优化资源。按实际使用的资源收费,无需为闲置资源付费。可以减少管理员的工作量,降低运维成本。

Preemptible VMs

  • 功能:在 GKE 中,可以使用 Preemptible VMs(抢占式虚拟机)来替代标准的 VM,这些实例价格较低,但如果 Google 需要资源,会随时中断这些实例。
  • 节省成本的方式:适用于非持久性、弹性强的负载,如批处理任务或容错能力强的应用程序。大幅降低计算成本,特别是在高负载情况下。

Cluster Autoscaler

  • 功能:GKE 支持 Cluster Autoscaler,它自动调整集群中的节点数,确保集群始终拥有足够的节点来满足工作负载需求。
  • 节省成本的方式: 自动扩展和缩减节点池,避免在低负载时浪费资源。

2. EKS(Amazon Elastic Kubernetes Service)

EKS Compute Optimizer

  • 功能:EKS Compute Optimizer 帮助分析您集群中的节点,并建议优化的实例类型,帮助您减少资源浪费。
  • 节省成本的方式: 根据计算需求自动选择最合适的实例类型,从而提高计算资源的利用率,减少不必要的开支。

EC2 Spot Instances

  • 功能:EKS 支持 EC2 Spot Instances,它是基于 EC2 实例的空闲容量提供的低价计算实例,但这些实例随时可能被中断。
  • 节省成本的方式:适用于批量处理、容错和非持续性负载,能够大幅降低计算成本。在 Spot 实例不可用时,自动迁移工作负载到其他实例。

Node Auto Scaling

  • 功能:EKS 提供 Node Auto Scaling,自动根据工作负载的需求调整集群中 EC2 实例的数量。
  • 节省成本的方式:节省不必要的开支,在低负载时减少节点数量,避免支付闲置资源的费用。

Fargate for EKS

  • 功能AWS Fargate 是 AWS 的一种无服务器计算引擎,允许用户在 EKS 中运行容器而无需管理 EC2 实例。
  • 节省成本的方式:按照容器的实际资源消耗计费,只为实际使用的 CPU 和内存付费,而不是为空闲的 EC2 实例付费。 提供更精细的资源利用率,避免低效的资源配置。

控制 k8s 集群成本的通用做法

  • Resource Requests and Limits
  • Resource Quotas
  • 多租户
  • 选择合适的节点实例
  • billing,如 AWS Cost Explorer, GCP Cost Management, Azure Cost Management
  • KubeCost, 一个用于 Kubernetes 成本管理和优化的开源工具
  • Cluster Autoscaler,根据集群中节点的负载自动调整节点的数量,根据 Pod 的调度需求和节点的资源使用情况来增加或删除节点
  • Vertical Pod Autoscaler & Horizontal Pod Autoscaler
  • Prometheus + Grafana 监控集群资源利用情况

kafka 在 k8s 中的部署,扩容和参数调优

使用 Helm 部署 Kafka,可以修改 replicaCount 参数,或者手动 scale up pod

kubectl scale statefulset kafka --replicas=5 -n kafka

扩容 Kafka Broker 后,如果需要增加 Kafka topic 的 partition 数量,可以使用 Kafka 的命令行工具。

kubectl run kafka-client -it --rm --image confluentinc/cp-kafka:latest --namespace kafka -- \
  kafka-topics --alter --topic <topic-name> --partitions <new-partition-count> --bootstrap-server kafka-service:9092

调整 Kafka Broker 参数配置

log.retention.hours: 168  # 默认保留7天的数据
num.io.threads: 8 # 设置 IO 线程的数量,影响 Kafka 的磁盘操作性能。如果磁盘性能是瓶颈,可以增加这个值
log.segment.bytes: 1073741824  # 1GB,控制 Kafka 日志分段的大小。适当调整这个值可以控制 Kafka 日志文件的大小。
log.cleaner.enable: true # 开启日志清理(Log Compaction

调整 Kafka 消费者和生产者配置

acks: all # 生产者可以选择不同的确认级别来保证消息的可靠性。设置为 `all` 是最安全的,但会增加延迟
linger.ms: 5 #  该参数设置在发送请求之前,生产者等待更多消息的时间。增加此值可以提高批量发送的效率,但会增加延迟
group.id: my-consumer-group # 消费者配置,确保消费者组的 `group.id` 配置正确,以便在同一组内多个消费者之间分配负载

Redis 大 key 问题

Redis 中存在单个键(key)占用大量内存或包含过多数据的情况。这种问题可能导致性能下降、内存分配不均甚至节点不可用。 通常是以下两种类型:

  • 单个字符串类型的大 key:一个键对应的值是非常大的字符串,例如超过几 MB
  • 集合类型的大 key(Hash、List、Set、ZSet): 一个键包含大量的元素,例如一个 List 中包含上百万条数据 一个大 key 可能占用过多内存,导致 Redis 剩余内存不足,影响其他数据存储或触发数据淘汰机制。 操作大 key(如 GETSETDELLRANGE 等)会导致更高的时间复杂度,增加延迟。 传输大 key 的值需要消耗大量带宽,影响客户端与 Redis 的交互性能。

如何发现:

# 使用 `MEMORY USAGE` 查看某个键的内存占用:
MEMORY USAGE <key>

DEBUG OBJECT <key>
SCAN 0 MATCH * COUNT 1000
MEMORY USAGE <key>  # 对每个键检查

# 使用 `redis-cli --bigkeys` 自动扫描 Redis 中的大key。
redis-cli --bigkeys

使用监控工具监控。

如何解决:

  • 优化存储结构:如果是字符串大 key,检查是否可以压缩数据(如 JSON 转为二进制、使用 Protobuf)。对集合类型的大 key,拆分为多个小集合。
  • 限制大 key 的写入:设置合理的键值大小限制,避免单个键值过大。
  • 分批读取和写入:对于需要操作大集合时,使用分页方式处理,例如 LRANGE 分段读取,而非一次性读取所有数据
  • 使用异步删除(Redis >= 4.0 提供的 UNLINK 命令)
  • 使用 Redis 集群分布存储,定期扫描 Redis 数据库,发现和优化大 key,设置键的过期时间(TTL)防止无用的大 key 长期存在。

解释 Raft 算法

关键词:leader,follower,随机过期时间,半数同意的选举,任期,日志一致性,next_index

  • 每个节点有可能的 2 个角色:leader 和 follower。一开始都是 follower。
  • follower 没有收到 leader 的心跳报文且随机过期时间过期则发起选举,任期加 1。
  • 其他 follower 投票给它最先收到选举报文,如果同时收到则响应任期大的那个,如果任期还相同者选择 next_index 大的那个。
  • follower 收到集群半数以上的投票即成为 leader,原 leader 因为任期小称为 follower。
  • 新的 leader 产生或者新的成员加入首先要做的事是确保集群中的日志一致性,通过 next_index 将 leader 的自己的有效日志设为集群的日志并复制日志给缺失日志的 follower。 参考:动画:Raft算法Leader选举、脑裂后选举、日志复制、修复不一致日志和数据安全_哔哩哔哩_bilibili

Dynamic CDN

动态 CDN 是一种内容分发网络(CDN)的实现方式,旨在优化动态内容的交付,如实时数据、数据库查询结果或动态生成的页面。与静态内容(如图片、CSS 文件、JavaScript)不同,动态内容通常是由服务器根据用户请求或其他因素生成,因此不能直接缓存到 CDN 节点上。

  1. 动态内容缓存:传统 CDN 主要用于缓存静态资源,而动态 CDN 专注于动态内容的加速。尽管动态内容不能像静态内容那样直接缓存,但 CDN 通过智能缓存策略、边缘计算和负载均衡来优化动态内容的传输。
  2. 请求路由和加速:动态 CDN 会将用户的请求路由到离用户最近的数据中心或边缘节点,尽量减少回源请求的次数。这可以减少源服务器的负载并提升响应速度。
  3. 智能路由和请求优化:基于地理位置、请求频率、数据源负载等因素,动态 CDN 会智能选择源站或边缘节点,以降低延迟和带宽消耗。

一般会选择 CDN 提供商:像 Cloudflare、Akamai、Fastly 等云服务商提供支持动态内容加速的 CDN 服务,自己实现的成本很高。 使用场景:

  1. 实时数据:例如,实时价格、新闻、体育比赛结果等需要快速更新的内容。
  2. 个性化内容:根据用户的身份、兴趣或行为生成的个性化内容,例如用户仪表板、推荐系统等。
  3. 动态生成的页面:例如电商网站的购物车、结账流程等,这些页面可能包含根据实时数据生成的动态内容。

Node not ready 了 pod 的容器会被自动 kill ?

当一个节点(Node)进入 NotReady 状态时,默认情况下,节点上的容器(Pod)不会立即被自动杀死或删除

k8s 的控制逻辑:

  • kube-controller-manager 会通过 kubelet 和节点的心跳信号(Heartbeat)监控节点的状态。
  • 如果节点未响应(例如网络中断、节点宕机等),节点会被标记为 NotReady
  • 即使节点被标记为 NotReady,节点上的容器并不会立即被停止或删除,因为节点可能只是暂时的网络故障。
  • NotReady 状态会触发 Pod 的调度控制逻辑,控制平面可能会迁移 Pod 到其他健康节点
  • node-monitor-grace-period: 控制平面在标记节点为 NotReady 之前等待的时间。默认值为 40 秒,如果节点超过此时间未发送心跳信号,则会被标记为 NotReady
  • pod-eviction-timeout: 当节点被标记为不可用后,控制平面会在 默认 5 分钟(300 秒)内尝试将该节点上的 Pod 驱逐(Eviction)。如果超过此时间,Pod 会被标记为失败并重新调度到其他节点。

节点关闭 | Kubernetes 如果触发了 kubelet 对 pod 的终止,则会走 Pod 的生命周期 | Kubernetes 中的 pod 终止流程,即走 preStop 钩子然后发停止信号给容器中的主进程,但就目前实际的工作经验来说,这个机制仍然不能完全保证在 node 出现问题后容器一定会被停止,还是需要通过 kubectl delete –grace-period 来强制删除,甚至是要重启 node 来解决,因为有时候 node 有问题到 kubelet 进程和 sshd 进程都受到了影响。

SSH 不了服务器时怎么排查

# 检查网络是否可达
ping -c 4 <target_host> 

# 检查 sshd 服务是否启动
sudo systemctl status sshd  # 如果是非 systemd 系统,可以尝试 service sshd status

# 检查是否开启了 22 端口
sudo netstat -tuln | grep ':22'  # 检查 22 端口的监听状态
# 或者
sudo ss -tuln | grep ':22'

# 检查用户的 SSH 权限
sudo cat /etc/ssh/sshd_config | grep -iE "AllowUsers|AllowGroups|DenyUsers|DenyGroups"  # 查看权限限制
grep $(whoami) /etc/passwd  # 检查当前用户是否存在
sudo grep $(whoami) /etc/ssh/sshd_config  # 检查是否有明确的用户限制

# 检查用户是否有 .ssh 目录和公私钥
ls -l ~/.ssh  # 列出密钥文件

进程,线程和协程

为什么协程比线程轻量? 协程是用户空间的线程,golang runtime 实现了在用户空间调度 goruntine,而线程的调度需要在内核空间进行,可以了解下 Go 的调度模型 GMP。第3讲-GMP模型简介_哔哩哔哩_bilibili

用 go 写 LRU

package main

import (
	"container/list"
	"fmt"
)

type LRUCache struct {
	capacity int
	cache    map[int]*list.Element
	list     *list.List
}

type pair struct {
	key   int
	value int
}

func NewLRUCache(cap int) *LRUCache {
	return &LRUCache{
		capacity: cap,
		cache:    make(map[int]*list.Element),
		list:     list.New(),
	}
}

func (c *LRUCache) Get(key int) int {
	if elem, ok := c.cache[key]; ok {
		c.list.MoveToFront(elem)
		return elem.Value.(pair).value
	}
	return -1
}

func (c *LRUCache) Put(key int, value int) {
	if elem, ok := c.cache[key]; ok {
		c.list.MoveToFront(elem)
		elem.Value = pair{key, value}
		return
	}

	if c.list.Len() == c.capacity {
		back := c.list.Back()
		if back != nil {
			c.list.Remove(back)
			delete(c.cache, back.Value.(pair).key)
		}
	}

	elem := c.list.PushFront(pair{key, value})
	c.cache[key] = elem
}

// test
func main() {
	lru := NewLRUCache(2)
	lru.Put(1, 1)
	lru.Put(2, 2)
	fmt.Println(lru.Get(1)) // Output: 1
	lru.Put(3, 3)           // Evicts key 2
	fmt.Println(lru.Get(2)) // Output: -1 (not found)
}

计算 k8s 集群 cpu,内存利用率

感觉这题作为面试题怪怪的,不过还是问了下 ai

kubectl top nodes --no-headers | awk '
{
  cpu_sum += $2
  cpu_percent_sum += $3
  mem_sum += $4
  mem_percent_sum += $5
}
END {
  print "Total CPU used:", cpu_sum " millicores"
  print "Average CPU usage:", cpu_percent_sum / NR "%"
  print "Total Mem used:", mem_sum
  print "Average Mem usage:", mem_percent_sum / NR "%"
}'

`NR` 是 AWK 中的一个**内置变量**,代表:

> **Number of Records**  
> 当前处理的是第几行,也可以理解为**总行数**,在上面就是可以用来计算出总共有多少node

python 中 log() vs print(), print to file

项目print()logging 模块 (log())
用途临时调试,输出到终端正式记录程序行为、错误、调试信息等
输出位置标准输出(屏幕)控制台 / 文件 / 网络 / 邮件(可配置)
可控等级有日志等级:DEBUG / INFO / WARNING / ERROR / CRITICAL
是否可禁用不容易可通过配置禁用某个等级以下的日志
格式手动拼接支持统一格式(时间戳、线程、函数名等)
print() 输出到文件:
with open("output.txt", "w") as f:
    print("Hello, file!", file=f)

# or config in global
import sys

sys.stdout = open("log.txt", "w")
print("This goes to the file now.")

解释 python 的 contextmanager

Context manager(上下文管理器) 是一种在进入和退出某个代码块时自动执行特定操作的机制。 最常见的例子是 with open(...):进入时打开文件,退出时自动关闭文件。

自动资源管理(比如文件、锁、数据库连接) 避免忘记释放资源 代码更简洁、安全、可读性更强

usage

with open("test.txt", "r") as f:
    data = f.read()
    
# or use with contextlib
from contextlib import contextmanager

@contextmanager
def my_resource():
    print("Enter")
    yield "resource"
    print("Exit")

with my_resource() as res:
    print("Using", res)

python generator,为什么要用

Generator(生成器) 是一种特殊的函数,用来一边计算、一边返回值不一次性返回全部数据。 用 yield 语句替代 return,函数会变成生成器。

def count_up_to(n):
    i = 1
    while i <= n:
        yield i
        i += 1

for x in count_up_to(3):
    print(x)
传统函数(return)Generator(yield)
一次性返回所有结果一次只返回一个值
占用大量内存(特别是大数据)节省内存(懒加载
适合小数据量适合大数据量 / 无限数据流

生成器(generator)是一种特殊类型的迭代器(iterator)。Generator 是 Python 提供的快速写迭代器的方法,是 iterator 的“语法糖”。

git merge vs git rebase

项目git mergegit rebase
做什么合并两个分支,保留各自的提交历史把一个分支的提交**“搬到”另一个分支上**
历史记录保留分支点,产生多条分支线(图像复杂)会让历史看起来像是一条直线
是否创建新提交是,会创建一个新的 merge commit否(除非有冲突),只是变更 commit 的 base
使用场景多人协作时,保留历史记录更清晰提交前整理历史记录,让提交更干净
是否改变原提交✅ 会修改提交(replay)
merge 适合保留完整历史rebase 适合整理历史、让 commit 更清晰

python 来读取数据库用多线程 or 多进程

多线程 更合适,因为数据库 IO 是IO 密集型任务,多线程可以并发请求,不受 GIL 限制。

对比项多线程(threading多进程(multiprocessing
GIL 影响有,但对 IO 操作影响小不受 GIL 限制
启动开销小(线程轻量)大(进程重量)
适合任务类型IO 密集型(如访问数据库)计算密集型(如图像处理)
数据共享简单(共享内存)复杂(需使用队列、Pipe 等)
# example
import threading
import sqlite3
import time

def fetch_from_db(thread_id):
    conn = sqlite3.connect("example.db")
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users")
    rows = cursor.fetchall()
    print(f"[Thread {thread_id}] Got {len(rows)} rows")
    conn.close()

# 准备数据库(模拟用)
def prepare_db():
    conn = sqlite3.connect("example.db")
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    cursor.executemany("INSERT INTO users (name) VALUES (?)", [("Alice",), ("Bob",), ("Carol",)])
    conn.commit()
    conn.close()

# 启动多个线程并发读取
def main():
    prepare_db()
    threads = []
    for i in range(5):
        t = threading.Thread(target=fetch_from_db, args=(i,))
        t.start()
        threads.append(t)

    for t in threads:
        t.join()

if __name__ == "__main__":
    main()

python 浅拷贝深拷贝相关的代码题

import copy
a = [3, 4]
l = [1,2, a] # [1,2,[3,4]]

from copy import copy
l1=copy(l) # shallow copy, l1: [1,2,[3,4]]
a.append(5) # [3,4,5], l1 is [1,2,[3,4,5]]
l.append(6) # 

print(l1) # [1,2,[3,4,5]]

# copy() 是浅拷贝(shallow copy)。
# 外层列表分开,内层引用共享。
# 修改内层(a.append(5))会影响两者。
# 修改外层(l.append(6))只影响原列表。
# Python 浅拷贝并不会修改外层列表,而是会创建一个新的列表,但新列表中的元素仍是原列表中子对象的引用。 因此,如果你修改的是新列表中的子对象(如列表),那么原列表的子对象也会被修改,因为它们指向同一个内存地址。 但如果你用 `append` 往新列表的外层添加一个全新的、与原列表无关的元素,则不会影响原列表的外层。

python 写 fetch all db data , 自定义 error

import sqlite3

# 自定义异常
class EmptyDataError(Exception):
    pass

def fetch_all_data():
    conn = sqlite3.connect("example.db")
    cursor = conn.cursor()

    cursor.execute("SELECT * FROM users")
    rows = cursor.fetchall()

    conn.close()

    if not rows:
        raise EmptyDataError("No data found in DB")
    return rows

# 初始化数据库数据(可选)
def prepare_db():
    conn = sqlite3.connect("example.db")
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    cursor.executemany("INSERT INTO users (name) VALUES (?)", [("Alice",), ("Bob",)])
    conn.commit()
    conn.close()

# 测试
if __name__ == "__main__":
    prepare_db()

    try:
        data = fetch_all_data()
        for row in data:
            print(row)
    except EmptyDataError as e:
        print("Error:", e)