aneasystone's blog - 云原生
type: channel
link: https://www.aneasystone.com/category/cloud-native/
items: 在 Kubernetes 中调度 GPU 资源
items: 学习 Kubernetes 流量管理之 Ingress
items: 学习 Kubernetes 流量管理之 Service
items: 容器运行时 containerd 学习笔记
items: 基于 Argo CD 的 GitOps 实践笔记
items: 使用 Istio 和 Envoy 打造 Service Mesh 微服务架构
items: gRPC 快速入门
items: 实战 Docker 容器网络
items: 使用 Helm 部署 Kubernetes 应用
items: 构建多架构容器镜像实战

在 Kubernetes 中调度 GPU 资源
type: item
date: 2023-12-03T08:21:00+00:00
description: 在人工智能越来越普及的今天，GPU 也变得越来越常见，无论是传统的机器学习和深度学习，还是现在火热的大语言模型和文生图模型，GPU 都是绕不开的话题。最近在工作中遇到一个需求，需要在 Kubernetes 中动态地调度和使用 GPU 资源，关于 GPU 这块一直是我的知识盲区，于是趁着业余时间恶补下相关的知识。GPU 环境准备学习 GPU 有一定的门槛，不仅是因为好点的显卡都价格不菲，而且使用它还要搭配有相应的硬件环境，虽然笔记本也可以通过显卡扩展坞来使用，但是性能有一定的损失。对于有条件的同学，网上有很多关于如何搭建自己的深度学习工作站的教程可供参考，对此我也没有什么经验，此处略过；对于没有条件的同学，网上也有很多白嫖 GPU 的攻略，我在 使用 Google Colab 体验 AI 绘画 这篇博客中也介绍了如何在 Google Colab 中免费使用 GPU 的方法；不过这些环境一般都是做机器学习相关的实验，如果想在上面做一些更底层的实验，比如安装 Docker，部署 Kubernetes 集群等，就不太合适了。正在无奈之际，我突然想到了阿里云的云服务器 ECS 有一个按量付费的功能，于是便上去瞅了瞅，发现有一种规格叫 共享型 GPU 实例，4 核 CPU，8G 内存，显卡为 NVIDIA A10，显存 2G，虽然配置不高，但是足够我们做实验的了，价格也相当便宜，一个小时只要一块八：于是便抱着试一试的态度下了一单，然后开始了下面的实验。但是刚开始就遇到了问题，安装 NVIDIA 驱动的时候一直报 Unable to load the kernel module 'nvidia.ko' 这样的错误:在网上搜了很多解决方案都没有解决，最后才在阿里云的产品文档中找到了答案：阿里云的 GPU 产品有 计算型 和 虚拟化型 两种实例规格族，可以从它们的命名上进行区分，比如上面我买的这个实例规格为 ecs.sgn7i-vws-m2s.xlarge，其中 sgn 表示这是一台采用 NVIDIA GRID vGPU 加速的共享型实例，它和 vgn 一样，都属于虚拟化型，使用了 NVIDIA GRID 虚拟 GPU 技术，所以需要安装 GRID 驱动，具体步骤可以 参考这里；如果希望手工安装 NVIDIA 驱动，我们需要购买计算型的 GPU 实例。阿里云的产品文档中有一篇 NVIDIA 驱动安装指引，我觉得整理的挺好，文档中对不同的规格、不同的使用场景、不同的操作系统都做了比较详情的介绍。于是我重新下单，又买了一台规格为 ecs.gn5i-c2g1.large 的 计算型 GPU 实例，2 核 CPU，8G 内存，显卡为 NVIDIA P4，显存 8G，价格一个小时八块多。购买计算型实例纯粹是为了体验一下 NVIDIA 驱动的安装过程，如果只想进行后面的 Kubernetes 实验，直接使用虚拟化型实例也是可以的。另外，在购买计算型实例时可以选择自动安装 NVIDIA 驱动，对应版本的 CUDA 和 CUDNN 也会一并安装，使用还是很方便的。安装 NVIDIA 驱动登录刚买的服务器，我们可以通过 lspci 看到 NVIDIA 的这张显卡：# lspci | grep NVIDIA 00:07.0 3D controller: NVIDIA Corporation GP104GL [Tesla P4] (rev a1)此时这个显卡还不能直接使用，因为还需要安装 NVIDIA 的显卡驱动。访问 NVIDIA Driver Downloads，在这里选择你的显卡型号和操作系统并搜索：从列表中可以看到驱动的不同版本，第一条是最新版本 535.129.03，我们点击链接进入下载页面并复制链接地址，然后使用下面的命令下载之：# curl -LO https://us.download.nvidia.com/tesla/535.129.03/NVIDIA-Linux-x86_64-535.129.03.run这个文件其实是一个可执行文件，直接运行即可：# sh NVIDIA-Linux-x86_64-535.129.03.run安装过程中会出现一些选项，保持默认即可，等待驱动安装成功后，运行 nvidia-smi 命令应该能看到显卡状态：# nvidia-smi Thu Nov 24 08:16:38 2023 +---------------------------------------------------------------------------------------+ | NVIDIA-SMI 535.129.03 Driver Version: 535.129.03 CUDA Version: 12.2 | |---------…

学习 Kubernetes 流量管理之 Ingress
type: item
date: 2023-11-21T00:00:00+00:00
description: 在 学习 Kubernetes 流量管理之 Service 这篇笔记中我们学习了 Kubernetes 是如何使用 Service 进行流量管理的，我们可以通过 NodePort 和 LoadBalancer 这两种类型的 Service 让应用程序暴露到集群外部，不过这两种方式都有各自的问题：NodePort 会在所有节点上暴露端口，外部应用需要知道集群内部节点的 IP 才能访问，一旦集群节点发生变化，外部应用也会受影响，可用性无法保证；而且端口的范围是受限的，默认只能使用 30000 到 32767 之间的端口，外部应用使用起来会感觉怪怪的；另外，每个端口只能对应一个 Service，如果 Service 数量过多，暴露的端口也会过多，不仅安全性难以保障，而且管理起来也会很麻烦；LoadBalancer 依赖于外部负载均衡器作为流量的入口，它在云平台中使用非常广泛，一般使用云供应商提供的 LB 服务，它会有一个自己的 IP 地址来转发所有流量，不过要注意的是，你暴露的每个 Service 都对应一个 LB 服务，而每个 LB 都需要独立付费，如果你暴露的 Service 很多，这将是非常昂贵的。什么是 Ingress为了解决上面的问题，Kubernetes 提出了一种新的 API 对象，叫做 Ingress，它通过定义不同的 HTTP 路由规则，将集群内部的 Service 通过 HTTP 的方式暴露到集群外部：可以将 Ingress 理解为 Service 的网关，它是所有流量的入口，通过 Ingress 我们就能以一个集群外部可访问的 URL 来访问集群内部的 Service，不仅如此，它还具有如下特性：Load BalancingSSL TerminationName-based Virtual HostingIngress 实践这一节将继续延用之前的 kubernetes-bootcamp 示例，通过 Ingress 将应用程序暴露到集群外部访问。部署 Ingress ControllerIngress 本身其实并不具备集群内外通信的能力，它只是一系列的路由转发规则而已，要让这些路由规则生效，必须先部署 Ingress Controller 才行。由 Kubernetes 支持和维护的 Ingress Controller 有三个：Ingress NGINX ControllerAWS Load Balancer ControllerGLBC除此之外，这里 还列出了很多由第三方社区维护的其他 Ingress Controller 可供选择。下面我们就以 Ingress NGINX Controller 为例，学习如何部署 Ingress Controller。目前有两个基于 Nginx 实现的 Ingress Controller 比较有名，一个是由 Kubernetes 官方维护的 kubernetes/ingress-nginx，被称为 Ingress NGINX Controller，另一个是由 Nginx 官方维护的 nginxinc/kubernetes-ingress，被称为 NGINX Ingress Controller，两者在技术实现和功能特性上有很多区别，大家在使用时要特别留意。根据 Ingress NGINX Controller 官方的部署文档，我们大致有两种方式来部署它，第一种是通过 Helm 部署：# helm upgrade --install ingress-nginx ingress-nginx \ --repo https://kubernetes.github.io/ingress-nginx \ --namespace ingress-nginx --create-namespace第二种是通过 kubectl apply 部署，我比较喜欢这种方式，可以从 YAML 中看到整个部署的细节：# kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/cloud/deploy.yaml namespace/ingress-nginx created serviceaccount/ingress-nginx created serviceaccount/ingress-nginx-admission created role.rbac.authorization.k8s.io/ingress-nginx created role.rbac.authorization.k8s.io/ingress-nginx…

学习 Kubernetes 流量管理之 Service
type: item
date: 2023-10-22T01:39:00+00:00
description: 在 Kubernetes 使用小记 这篇笔记中我们学习了 Kubernetes 的基本用法和概念，通过 Deployment 部署应用程序，然后通过 Service 将应用程序暴露给其他人访问。其中 Service 是 Kubernetes 最基础的流量管理机制之一，它的主要目的有：以一个固定的地址来访问应用程序；实现多个副本之间的负载均衡；让应用程序可以在集群外部进行访问；这篇笔记将继续使用之前的示例，通过一系列的实验更进一步地学习 Service 的工作原理。准备实验环境首先，创建一个 Deployment 部署应用程序，这里直接使用之前示例中的 jocatalin/kubernetes-bootcamp:v1 镜像，副本数设置为 3：apiVersion: apps/v1 kind: Deployment metadata: labels: app: myapp version: v1 name: myapp spec: replicas: 3 selector: matchLabels: app: myapp version: v1 template: metadata: labels: app: myapp version: v1 spec: containers: - image: jocatalin/kubernetes-bootcamp:v1 name: myapp等待三个副本都启动成功：# kubectl get deploy myapp -o wide NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR myapp 3/3 3 3 13m myapp jocatalin/kubernetes-bootcamp:v1 app=myapp然后创建一个 Service：apiVersion: v1 kind: Service metadata: labels: app: myapp name: myapp spec: ports: - port: 38080 targetPort: 8080 selector: app: myapp type: ClusterIP通过 kubectl get svc 查询 Service 的地址和端口：# kubectl get svc myapp -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR myapp ClusterIP 10.96.3.215 <none> 38080/TCP 7m22s app=myapp通过 Service 的地址验证服务能正常访问，多请求几次，可以看到会自动在副本之间轮询访问：# curl 10.96.3.215:38080 Hello Kubernetes bootcamp! | Running on: myapp-fdb95659d-fl5c4 | v=1 # curl 10.96.3.215:38080 Hello Kubernetes bootcamp! | Running on: myapp-fdb95659d-dd5vv | v=1 # curl 10.96.3.215:38080 Hello Kubernetes bootcamp! | Running on: myapp-fdb95659d-4xf4g | v=1 # curl 10.96.3.215:38080 Hello Kubernetes bootcamp! | Running on: myapp-fdb95659d-fl5c4 | v=1Service 配置细节上面是一个简单的 Service 示例，这一节对其配置参数进行详细说明。端口配置在上面的 Service 定义中，第一个重要参数是 spec.ports 端口配置：spec: ports: - port: 38080 targetPort: 8080其中 port 表示 Service 的端口，targetPort 表示 Pod 的端口。Service 创建成功之后，Kubernetes 会为该 Service 分配一个 IP 地址，Service 从自己的 IP 地址和 port 端口接收请求，并将请求映射到符合条件的 Pod 的 targetPort。多端口配置可以在一个 Service 对象中定义多个端口，此时，我们必须为每个端口定义一个名字：spec: ports: - name: http port: 38080 targetPort: 8080 - name: https port: 38083 targetPort: 8083协议配置此外，可以给 Servi…

容器运行时 containerd 学习笔记
type: item
date: 2023-06-24T01:03:00+00:00
description: 2016 年 12 月，Docker 公司宣布将 containerd 项目从 Docker Engine 中分离出来，形成一个独立的开源项目，并捐赠给 CNCF 基金会，旨在打造一个符合工业标准的容器运行时。Docker 公司之所以做出这样的决定，是因为当时在容器编排的市场上 Docker 面临着 Kubernetes 的极大挑战，将 containerd 分离，是为了方便开展 Docker Swarm 项目，不过结果大家都知道，Docker Swarm 在 Kubernetes 面前以惨败收场。containerd 并不是直接面向最终用户的，而是主要用于集成到更上层的系统里，比如 Docker Swarm、Kubernetes 或 Mesos 等容器编排系统。containerd 通过 unix domain docket 暴露很低层的 gRPC API，上层系统可以通过这些 API 对机器上的容器整个生命周期进行管理，包括镜像的拉取、容器的启动和停止、以及底层存储和网络的管理等。下面是 containerd 官方提供的架构图：从上图可以看出，containerd 的核心主要由一堆的 Services 组成，通过 Content Store、Snapshotter 和 Runtime 三大技术底座，实现了 Containers、Content、Images、Leases、Namespaces 和 Snapshots 等的管理。其中 Runtime 部分和容器的关系最为紧密，可以看到 containerd 通过 containerd-shim 来支持多种不同的 OCI runtime，其中最为常用的 OCI runtime 就是 runc，所以只要是符合 OCI 标准的容器，都可以由 containerd 进行管理，值得一提的是 runc 也是由 Docker 开源的。OCI 的全称为 Open Container Initiative，也就是开放容器标准，主要致力于创建一套开放的容器格式和运行时行业标准，目前包括了 Runtime、Image 和 Distribution 三大标准。containerd 与 Docker 和 Kubernetes 的关系仔细观察 containerd 架构图的上面部分，可以看出 containerd 通过提供 gRPC API 来供上层应用调用，上层应用可以直接集成 containerd client 来访问它的接口，诸如 Docker Engine、BuildKit 以及 containerd 自带的命令行工具 ctr 都是这样实现的；所以从 Docker 1.11 开始，当我们执行 docker run 命令时，整个流程大致如下：Docker Client 和 Docker Engine 是典型的 CS 架构，当用户执行 docker run 命令时，Docker Client 调用 Docker Engine 的接口，但是 Docker Engine 并不负责容器相关的事情，而是调用 containerd 的 gRPC 接口交给 containerd 来处理；不过 containerd 收到请求后，也并不会直接去创建容器，因为我们在上面提到，创建容器实际上已经有一个 OCI 标准了，这个标准有很多实现，其中 runc 是最常用的一个，所以 containerd 也不用再去实现这套标准了，而是直接调用这些现成的 OCI Runtime 即可。不过创建容器有一点特别需要注意的地方，我们创建的容器进程需要一个父进程来做状态收集、维持 stdin 等工作的，这个父进程如果是 containerd 的话，那么如果 containerd 挂掉的话，整个机器上的所有容器都得退出了，为了解决这个问题，containerd 引入了 containerd-shim 组件；shim 的意思是垫片，正如它的名字所示，它其实是一个代理，充当着容器进程和 containerd 之间的桥梁；每当用户启动容器时，都会先启动一个 containerd-shim 进程，containerd-shim 然后调用 runc 来启动容器，之后 runc 会退出，而 containerd-shim 则会成为容器进程的父进程，负责收集容器进程的状态，上报给 containerd，并在容器中 PID 为 1 的进程退出后接管容器中的子进程进行清理，确保不会出现僵尸进程。介绍完 containerd 与 Docker 之间的关系，我们再来看看它与 Kuberntes 的关系，从历史上看，Kuberntes 和 Docker 相爱相杀多年，一直是开源社区里热门的讨论话题。在 Kubernetes 早期的时候，由于 Docker 风头正…

基于 Argo CD 的 GitOps 实践笔记
type: item
date: 2023-05-07T22:32:00+00:00
description: GitOps 这个概念最早是由 Weaveworks 的 CEO Alexis Richardson 在 2017 年提出的，它是一种全新的基于 Git 仓库来管理 Kubernetes 集群和交付应用程序的方式。它包含以下四个基本原则：声明式（Declarative）：整个系统必须通过声明式的方式进行描述，比如 Kubernetes 就是声明式的，它通过 YAML 来描述系统的期望状态；版本控制和不可变（Versioned and immutable）：所有的声明式描述都存储在 Git 仓库中，通过 Git 我们可以对系统的状态进行版本控制，记录了整个系统的修改历史，可以方便地回滚；自动拉取（Pulled automatically）：我们通过提交代码的形式将系统的期望状态提交到 Git 仓库，系统从 Git 仓库自动拉取并做出变更，这种被称为 Pull 模式，整个过程不需要安装额外的工具，也不需要配置 Kubernetes 的认证授权；而传统的 CI/CD 工具如 Jenkins 或 CircleCI 等使用的是 Push 模式，这种模式一般都会在 CI 流水线运行完成后通过执行命令将应用部署到系统中，这不仅需要安装额外工具（比如 kubectl），还需要配置 Kubernetes 的授权，而且这种方式无法感知部署状态，所以也就无法保证集群状态的一致性了；持续调谐（Continuously reconciled）：通过在目标系统中安装一个 Agent，一般使用 Kubernetes Operator 来实现，它会定期检测实际状态与期望状态是否一致，一旦检测到不一致，Agent 就会自动进行修复，确保系统达到期望状态，这个过程就是调谐（Reconciliation）；这样做的好处是将 Git 仓库作为单一事实来源，即使集群由于误操作被修改，Agent 也会通过持续调谐自动恢复。其实，在提出 GitOps 概念之前，已经有另一个概念 IaC （Infrastructure as Code，基础设施即代码）被提出了，IaC 表示使用代码来定义基础设施，方便编辑和分发系统配置，它作为 DevOps 的最佳实践之一得到了社区的广泛关注。关于 IaC 和 GitOps 的区别，可以参考 The GitOps FAQ。Argo CD 快速入门基于 GitOps 理念，很快诞生出了一批 声明式的持续交付（Declarative Continuous Deployment） 工具，比如 Weaveworks 的 Flux CD 和 Intuit 的 Argo CD，虽然 Weaveworks 是 GitOps 概念的提出者，但是从社区的反应来看，似乎 Argo CD 要更胜一筹。Argo 项目最初是由 Applatix 公司于 2017 年创建，2018 年这家公司被 Intuit 收购，Argo 项目就由 Intuit 继续维护和演进。Argo 目前包含了四个子项目：Argo Workflows、Argo Events、Argo CD 和 Argo Rollouts，主要用于运行和管理 Kubernetes 上的应用程序和任务，所有的 Argo 项目都是基于 Kubernetes 控制器和自定义资源实现的，它们组合在一起，提供了创建应用程序和任务的三种模式：服务模式、工作流模式和基于事件的模式。2020 年 4 月 7 日，Argo 项目加入 CNCF 开始孵化，并于 2022 年 12 月正式毕业，成为继 Kubernetes、Prometheus 和 Envoy 之后的又一个 CNCF 毕业项目。这一节我们将学习 Argo CD，学习如何通过 Git 以及声明式描述来部署 Kubernetes 资源。安装 Argo CDArgo CD 提供了两种 安装形式：多租户模式（Multi-Tenant） 和 核心模式（Core）。多租户模式提供了 Argo CD 的完整特性，包括 UI、SSO、多集群管理等，适用于多个团队用户共同使用；而核心模式只包含核心组件，不包含 UI 及多租户功能，适用于集群管理员独自使用。使用比较多的是多租户模式，官方还为其提供了两份部署配置：非高可用配置 install.yaml 和高可用配置 ha/install.yaml，生产环境建议使用高可用配置，开发和测试环境可以使用非高可用配置，下面就使用非高可用配置来安装 Argo CD。首先，创建一个 argocd 命名空间：$ kubectl create namespace argocd namespace/argocd created接着将 Argo CD 部署到该命名空间中：$ kubectl apply -n argocd -f https://raw.gi…

使用 Istio 和 Envoy 打造 Service Mesh 微服务架构
type: item
date: 2023-04-05T23:11:00+00:00
description: 周志明 老师在他的 《凤凰架构》 中将分布式服务通信的演化历史分成五个阶段：第一阶段：将通信的非功能性需求视作业务需求的一部分，通信的可靠性由程序员来保障这个阶段是分布式系统发展最早期时的技术策略，这些系统一开始仅仅是通过 RPC 技术去访问远程服务，当遇到通信问题时，就在业务代码中引入相应的处理逻辑，比如服务发现、网络重试或降级等。这些通信的非功能性逻辑和业务逻辑耦合在一起，让系统变得越来越复杂。第二阶段：将代码中的通信功能抽离重构成公共组件库，通信的可靠性由专业的平台程序员来保障这个阶段人们逐渐意识到通信功能应该从业务逻辑中抽离出来，于是形成了一些公共组件库，比如 Finagle、Spring Cloud 等。不过这些组件库大多是和语言绑定的，比如 Spring Cloud 技术栈只能用在 Java 项目中，遇到其他语言的项目就无能为力了。第三阶段：将负责通信的公共组件库分离到进程之外，程序间通过网络代理来交互，通信的可靠性由专门的网络代理提供商来保障为了将通信功能做成语言无关的，这个阶段发展出了专门负责可靠通信的网络代理，比如 Netflix Prana。业务系统和这类代理之间通过回环设备或 Unix 套接字进行通信，网络代理对业务系统的网络流量进行拦截，从而在代理上完成流控、重试或降级等几乎所有的分布式通信功能。这种网络代理后来演化出了两种形态：一种是微服务网关，它位于整个分布式系统的入口，同时代理整个系统中所有服务的网络流量；另一种是边车代理，它和某个进程共享一个网络命名空间，专门针对性地代理该服务的网络流量。第四阶段：将网络代理以边车的形式注入到应用容器，自动劫持应用的网络流量，通信的可靠性由专门的通信基础设施来保障在使用上一阶段的网络代理时，我们必须在应用程序中手工指定网络代理的地址才能生效，而边车代理有一个很大的优势，它通过 iptables 等技术自动劫持代理进程的网络流量，所以它对应用是完全透明的，无需对应用程序做任何改动就可以增强应用的通信功能。目前边车代理的代表性产品有 Linkerd、Envoy、MOSN 等。不过，边车代理也有一个很大的缺点，随着系统中代理服务的增多，对边车代理的维护和管理工作就成了运维最头疼的问题，于是服务网格应运而生。第五阶段：将边车代理统一管控起来实现安全、可控、可观测的通信，将数据平面与控制平面分离开来，实现通用、透明的通信，这项工作就由专门的服务网格框架来保障服务网格（Service Mesh）一词是 Buoyant 公司的 CEO William Morgan 于 2017 年在他的一篇博客 《What's a service mesh? And why do I need one?》 中首次提出的，他是世界上第一款服务网格产品 Linkerd 的创始人之一，在博客中，William Morgan 对服务网格做了如下定义：服务网格是一种用于管控服务间通信的的基础设施，它负责为现代云原生应用程序在复杂服务拓扑中可靠地传递请求。在实践中，服务网格通常以轻量级网络代理阵列的形式实现，这些代理与应用程序部署在一起，对应用程序来说无需感知代理的存在。服务网格将上一阶段的边车代理联合起来形成如下所示的网格状结构：在服务网格中，主要由数据平面与控制平面组成。数据平面是所有边车代理的集合，负责拦截所有服务流入和流出的流量，并配置控制平面对流量进行管理；控制平面对数据平面进行管理，完成配置分发、服务发现、授权鉴权等功能。如上图所示，整个系统中的通信包括两个部分：实线表示数据平面之间的通信，虚线表示控制平面和数据平面之间的通信。服务网格的概念一经提出，其价值迅速被业界所认可，业界几乎所有的云原生玩家都积极参与了进来：2016 年，Buoyant 公司 推出 Linkerd，同年，Lyft 公司 推出 Envoy2017 年，Linkerd 加入 CNCF，同年，Google、IBM 和 Lyft 共同发布 Istio，为了和 Istio 展开竞争，Buoyant 公司将自家的 Conduit 产品合并到 Linkerd 中发布了 Linkerd 22018 年后，Google、亚马逊、微软分别推出各自的公有云版本 Service Mesh 产品，国内的阿里巴巴也推出了基于 Istio 的修改版 SOFAMesh（目前已经废弃），并开源了自己研发的 MOSN 代理随着各巨头的参与，Istio 逐渐超过了 Linkerd 的地位，几乎成了原云生环境下服务网格中控制平面的事实标准，而 Envoy 凭借其卓越的性能和强大的动态配置功能，成为了服务网格中数据平面的不二选择，下图是使用 Istio 作为服务网格方案后的典型架构：Istio 服务网格的数据平面由 Envoy 代理实现，在 Envoy 学习…

gRPC 快速入门
type: item
date: 2023-03-08T03:42:00+00:00
description: RPC 又被称为 远程过程调用，英文全称为 Remote Procedure Call，是一种服务间的通信规范，它可以让你像调用本地方法一样调用远程服务提供的方法，而不需要关心底层的通信细节。RPC 的概念早在上个世纪七八十年代就已经被提出，1984 年，Birrell 和 Nelson 在 ACM Transactions on Computer Systems 期刊上发表了一篇关于 RPC 的经典论文 Implementing remote procedure calls，论文中首次给出了实现 RPC 的基本框架：从这个框架中可以看到很多现代 RPC 框架的影子，比如客户端和服务端的 Stub、序列化和反序列化等，事实上，所有后来的 RPC 框架几乎都是源自于这个原型。不过在那个年代，RPC 的争议是非常大的，由于网络环境的不可靠性，RPC 永远都不可能做到像调用本地方法一样。大家提出了一堆问题，比如：故障恢复、请求重试、异步请求、服务寻址等，在那个互联网都还没有出现的年代，一堆大神们就已经在讨论分布式系统间的调用问题了，而他们讨论的问题焦点，基本上都演变成了 RPC 历史中永恒的话题。为了解决这些问题，软件架构经历了一代又一代的发展和演进。1988 年，Sun 公司推出了第一个商业化的 RPC 库 Sun RPC ，并被定义为标准的 RPC 规范；1991 年，非营利性组织 OMG 发布 CORBA，它通过接口定义语言 IDL 中的抽象类型映射让异构环境之间的互操作成为了可能；不过由于其复杂性，很快就被微软推出的基于 XML 的 SOAP 技术所打败，随后 SOAP 作为 W3C 标准大大推动了 Web Service 概念的发展；像 SOAP 这种基于 XML 的 RPC 技术被称为 XML-RPC，它最大的问题是 XML 报文内容过于冗余，对 XML 的解析效率也很低，于是 JSON 应运而生，进而导致 RESTful 的盛行；不过无论是 XML 还是 JSON，都是基于文本传输，性能都无法让人满意，直到 2008 年，Google 开源 Protocol Buffers，这是一种高效的结构化数据存储格式，可以用于结构化数据的序列化，非常适合做数据存储或 RPC 数据交换格式；可能是由于微服务的流行，之后的 RPC 框架如雨后春笋般蓬勃发展，同年，Facebook 向 Apache 贡献了开源项目 Thrift，2009 年，Hadoop 之父 Doug Cutting 开发出 Avro，成为 Hadoop 的一个子项目，随后又脱离 Hadoop 成为 Apache 顶级项目；2011 年，阿里也开源了它自研的 RPC 框架 Dubbo，和前两个一样，最后也贡献给了 Apache；2015 年，Google 开源 gRPC 框架，开创性地使用 HTTP/2 作为传输协议，基于 HTTP/2 的多路复用和服务端推送技术，gRPC 支持双向流式通信，这使得 RPC 框架终于不再拘泥于万年不变的 C/S 模型了。2017 年，gRPC 作为孵化项目成为 CNCF 的一员，不论是 Envoy 还是 Istio 等 Service Mesh 方案，都将 gRPC 作为一等公民，可以预见的是，谷歌正在将 gRPC 打造成云原生时代通信层事实上的标准。从 Hello World 开始这一节我们使用 Go 语言实现一个简单的 Hello World 服务，学习 gRPC 的基本概念。首先，我们通过 go mod init 初始化示例项目：$ mkdir demo && cd demo $ go mod init example.com/demo go: creating new go.mod: module example.com/demo go: to add module requirements and sums: go mod tidy然后获取 grpc 依赖：$ go get google.golang.org/grpc@latest go: downloading golang.org/x/net v0.5.0 go: downloading golang.org/x/sys v0.4.0 go: downloading google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f go: downloading golang.org/x/text v0.6.0 go: added github.com/golang/protobuf v1.5.2 go: added golang.org/x/net v0.5.0 go: added golang.org/x…

实战 Docker 容器网络
type: item
date: 2023-02-23T04:45:00+00:00
description: 我们知道，容器技术出现的初衷是对容器之间以及容器和宿主机之间的进程、用户、网络、存储等进行隔离，提供一种类似沙盒的虚拟环境，容器网络是这个虚拟环境的一部分，它能让应用从宿主机操作系统的网络环境中独立出来，形成容器自有的网络设备、IP 协议栈、端口套接字、IP 路由表、防火墙等模块。但是网络作为一种特殊的通信机制，我们有时候又希望容器之间、容器和宿主机之间甚至容器和远程主机之间能够互相通信，既要保证容器网络的隔离性，又要实现容器网络的连通性，这使得在容器环境下，网络的问题变得异常复杂。Docker 是目前最流行的容器技术之一，它提供了一套完整的网络解决方案，不仅可以解决单机网络问题，也可以实现跨主机容器之间的通信。容器网络模型在学习各种不同的容器网络解决方案之前，我们首先来了解下 CNM 的概念。CNM（Container Network Model） 是 Docker 提出并主推的一种容器网络架构，这是一套抽象的设计规范，主要包含三个主要概念：Sandbox - 提供了容器的虚拟网络栈，即端口套接字、IP 路由表、防火墙、DNS 配置等内容，主要用于隔离容器网络与宿主机网络，形成了完全独立的容器网络环境，一般通过 Linux 中的 Network Namespace 或类似的技术实现。一个 Sandbox 中可以包含多个 Endpoint。Endpoint - 就是虚拟网络的接口，就像普通网络接口一样，它的主要职责是创建 Sandbox 到 Network 之间的连接，一般使用 veth pair 之类的技术实现。一个已连接的 Endpoint 只能归属于一个 Sandbox 和 一个 Network。Network - 提供了一个 Docker 内部的虚拟子网，一个 Network 可以包含多个 Endpoint，同一个 Network 内的 Endpoint 之间可以互相通讯，一般使用 Linux bridge 或 VLAN 来实现。这三个概念之间的关系如下图所示：libnetwork 是一个 Go 语言编写的开源库，它是 CNM 规范的标准实现，Docker 就是通过 libnetwork 库来实现 CNM 规范中的三大概念，此外它还实现了本地服务发现、基于 Ingress 的容器负载均衡、以及网络控制层和管理层功能。除了控制层和管理层，我们还需要实现网络的数据层，这部分 Docker 是通过 驱动（Driver） 来实现的。驱动负责处理网络的连通性和隔离性，通过不同的驱动，我们可以扩展 Docker 的网络栈，实现不同的网络类型。Docker 内置如下这些驱动，通常被称作 原生驱动 或者 本地驱动：bridgehostnoneoverlayipvlanmacvlan第三方也可以通过 Network plugins 实现自己的网络驱动，这些驱动也被称作 远程驱动，比如 calico、flannel、weave 等。基于 Docker 网络的这种可插拔的设计，我们通过切换不同的网络驱动，就可以实现不同的容器网络解决方案。单机容器网络方案让我们从最简单的单机网络方案开始。Docker 在安装时，默认会在系统上创建三个网络，可以通过 docker network ls 命令查看：$ docker network ls NETWORK ID NAME DRIVER SCOPE 04eb1fccf2a8 bridge bridge local 189dfa3e3e64 host host local 1e63120a4e7a none null local这三个网络分别是 bridge、host 和 none，可以看到这三个网络都是 local 类型的。Bridge 网络Bridge 网络是目前使用最广泛的 Docker 网络方案，当我们使用 docker run 启动容器时，默认使用的就是 Bridge 网络，我们也可以通过命令行参数 --network=bridge 显式地指定使用 Bridge 网络：$ docker run --rm -it --network=bridge busybox / # ifconfig eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02 inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:9 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrie…

使用 Helm 部署 Kubernetes 应用
type: item
date: 2023-02-12T00:28:00+00:00
description: 在 Kubernetes 使用小记 中我们学习了如何通过 Deployment 部署一个简单的应用服务并通过 Service 来暴露它，在真实的场景中，一套完整的应用服务可能还会包含很多其他的 Kubernetes 资源，比如 DaemonSet、Ingress、ConfigMap、Secret 等，当用户部署这样一套完整的服务时，他就不得不关注这些底层的 Kubernetes 概念，这对用户非常不友好。我们知道，几乎每个操作系统都内置了一套软件包管理器，用来方便用户安装、配置、卸载或升级各种系统软件和应用软件，比如 Debian 和 Ubuntu 使用的是 DEB 包管理器 dpkg 和 apt，CentOS 使用的是 RPM 包管理器 yum，Mac OS 有 Homebrew，Windows 有 WinGet 和 Chocolatey 等，另外很多系统还内置了图形界面的应用市场方便用户安装应用，比如 Windows 应用商店、Apple Store、安卓应用市场等，这些都可以算作是包管理器。使用包管理器安装应用大大降低了用户的使用门槛，他只需要执行一句命令或点击一个安装按钮即可，而不用去关心这个应用需要哪些依赖和哪些配置。那么在 Kubernetes 下能不能也通过这种方式来部署应用服务呢？Helm 作为 Kubernetes 的包管理器，解决了这个问题。Helm 简介Helm 是 Deis 团队 于 2015 年发布的一个 Kubernetes 包管理工具，当时 Deis 团队还没有被微软收购，他们在一家名为 Engine Yard 的公司里从事 PaaS 产品 Deis Workflow 的开发，在开发过程中，他们需要将众多的微服务部署到 Kubernetes 集群中，由于在 Kubernetes 里部署服务需要开发者手动编写和维护数量繁多的 Yaml 文件，并且服务的分发和安装也比较繁琐，Matt Butcher 和另外几个同事于是在一次 Hackthon 团建中发起了 Helm 项目。Helm 的取名非常有意思，Kubernetes 是希腊语 “舵手” 的意思，而 Helm 是舵手操作的 “船舵”，用来控制船的航行方向。Helm 引入了 Chart 的概念，它也是 Helm 所使用的包格式，可以把它理解成一个描述 Kubernetes 相关资源的文件集合。开发者将自己的应用配置文件打包成 Helm chart 格式，然后发布到 ArtifactHub，这和你使用 docker push 将镜像推送到 DockerHub 镜像仓库一样，之后用户安装你的 Kubernetes 应用只需要一条简单的 Helm 命令就能搞定，极大程度上解决了 Kubernetes 应用维护、分发、安装等问题。Helm 在 2018 年 6 月加入 CNCF，并在 2020 年 4 月正式毕业，目前已经是 Kubernetes 生态里面不可或缺的明星级项目。快速开始这一节我们将通过官方的入门示例快速掌握 Helm 的基本用法。安装 Helm我们首先从 Helm 的 Github Release 页面找到最新版本，然后通过 curl 将安装包下载下来：$ curl -LO https://get.helm.sh/helm-v3.11.1-linux-amd64.tar.gz然后解压安装包，并将 helm 安装到 /usr/local/bin 目录：$ tar -zxvf helm-v3.11.1-linux-amd64.tar.gz $ sudo mv linux-amd64/helm /usr/local/bin/helm这样 Helm 就安装好了，通过 helm version 检查是否安装成功：$ helm version version.BuildInfo{Version:"v3.11.1", GitCommit:"293b50c65d4d56187cd4e2f390f0ada46b4c4737", GitTreeState:"clean", GoVersion:"go1.18.10"}使用 helm help 查看 Helm 支持的其他命令和参数。一般来说，直接下载 Helm 二进制文件就可以完成安装，不过官方也提供了一些其他方法来安装 Helm，比如通过 get_helm.sh 脚本来自动安装，或者通过 yum 或 apt 这些操作系统的包管理器来安装，具体内容可参考官方的 安装文档。使用 HelmHelm 安装完成之后，我们就可以使用 Helm 在 Kubernetes 中安装应用了。对于新手来说，最简单的方法是在 ArtifactHub 上搜索要安装的应用，然后按照文档中的安装步骤来操作即可。比如我们想要部署 Nginx，首…

构建多架构容器镜像实战
type: item
date: 2023-01-10T23:54:00+00:00
description: 最近在一个国产化项目中遇到了这样一个场景，在同一个 Kubernetes 集群中的节点是混合架构的，也就是说，其中某些节点的 CPU 架构是 x86 的，而另一些节点是 ARM 的。为了让我们的镜像在这样的环境下运行，一种最简单的做法是根据节点类型为其打上相应的标签，然后针对不同的架构构建不同的镜像，比如 demo:v1-amd64 和 demo:v1-arm64，然后还需要写两套 YAML：一套使用 demo:v1-amd64 镜像，并通过 nodeSelector 选择 x86 的节点，另一套使用 demo:v1-arm64 镜像，并通过 nodeSelector 选择 ARM 的节点。很显然，这种做法不仅非常繁琐，而且管理起来也相当麻烦，如果集群中还有其他架构的节点，那么维护成本将成倍增加。你可能知道，每个 Docker 镜像都是通过一个 manifest 来描述的，manifest 中包含了这个镜像的基本信息，包括它的 mediaType、大小、摘要以及每一层的分层信息等。可以使用 docker manifest inspect 查看某个镜像的 manifest 信息：$ docker manifest inspect aneasystone/hello-actuator:v1 { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 3061, "digest": "sha256:d6d5f18d524ce43346098c5d5775de4572773146ce9c0c65485d60b8755c0014" }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 2811478, "digest": "sha256:5843afab387455b37944e709ee8c78d7520df80f8d01cf7f861aae63beeddb6b" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 928436, "digest": "sha256:53c9466125e464fed5626bde7b7a0f91aab09905f0a07e9ad4e930ae72e0fc63" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 186798299, "digest": "sha256:d8d715783b80cab158f5bf9726bcada5265c1624b64ca2bb46f42f94998d4662" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 19609795, "digest": "sha256:112ce4ba7a4e8c2b5bcf3f898ae40a61b416101eba468397bb426186ee435281" } ] }可以加上 --verbose 查看更详细的信息，包括该 manifest 引用的镜像标签和架构信息：$ docker manifest inspect --verbose aneasystone/hello-actuator:v1 { "Ref": "docker.io/aneasystone/hello-actuator:v1", "Descriptor": { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "digest": "sha256:f16a1fcd331a6d196574a0c0721688360bf53906ce0569bda529ba09335316a2", "size": 1163, "platform": { "architecture": "amd64", "os": "linux" } }, "SchemaV2Manifest": { ... } }我们一般不会直接使用 manifest，而是通过标签来关联它，方便人们使用。…