golang 实现加载动态库

golang开发中需要用到插件动态加载技术,需要主程序动态加载一些功能模块,不需要修改主程序。相比于java ,java 可以通过加载class文件方式来实现模块加载,而golang通常都是单个二进制文件。 go-plugin则使用另外一种方式实现插件加载,主进程和插件进程是两个独立的进程,二者通过rpc/grpc的方式进行通信, 从而实现主进程调用插件进程。 其次,golang也原生提供一种插件模块加载机制plugins,下面依次进行介绍。 go-plugin 项目地址https://github.com/hashicorp/go-plugin 特征 插件是Go接口的实现:这让插件的编写、使用非常自然。对于插件的作者来说,他只需要实现一个Go接口即可;对于插件的用户来说,他只需要调用一个Go接口即可。go-plugin会处理好本地调用转换为gRPC调用的所有细节 跨语言支持:插件可以基于任何主流语言编写,同样可以被任何主流语言消费 支持复杂的参数、返回值:go-plugin可以处理接口、io.Reader/Writer等复杂类型 双向通信:为了支持复杂参数,宿主进程能够将接口实现发送给插件,插件也能够回调到宿主进程 内置日志系统:任何使用log标准库的的插件,都会将日志信息传回宿主机进程。宿主进程会在这些日志前面加上插件二进制文件的路径,并且打印日志 协议版本化:支持一个简单的协议版本化,增加版本号后可以基于老版本协议的插件无效化。当接口签名变化时应当增加版本 标准输出/错误同步:插件以子进程的方式运行,这些子进程可以自由的使用标准输出/错误,并且打印的内容会被自动同步到宿主进程,宿主进程可以为同步的日志指定一个io.Writer TTY Preservation:插件子进程可以链接到宿主进程的stdin文件描述符,以便要求TTY的软件能正常工作 宿主进程升级:宿主进程升级的时候,插件子进程可以继续允许,并在升级后自动关联到新的宿主进程 加密通信:gRPC信道可以加密 完整性校验:支持对插件的二进制文件进行Checksum 插件崩溃了,不会导致宿主进程崩溃 容易安装:只需要将插件放到某个宿主进程能够访问的目录即可 用法 这里我们通过一个实例来体验一下go-plugin插件开发。我们需要完成的功能是,主程序暴露了两个方法,插件模块去实现这两个方法,然后主程序加载插件完成功能实现。 最终达成的效果是通过Put方法设置一个key/value对,通过get方法获取key对应的值。 Put(key string, value []byte) error Get(key string) ([]byte, error) ([]byte, error) 新建项目 这里我先将完整项目目录结构列出来 . ├── Makefile ├── cmd │ ├── cmd │ ├── kv_wky │ └── main.go ├── pkg │ ├── plugins │ │ └── proto │ │ ├── plugin.pb.go │ │ └── plugin.proto │ └── shared │ ├── grpc.go │ └── plugin.go └── pluginclient ├── main.go └── pluginclient 定义proto 新建pkg/plugins/proto/plugin.proto文件,并生成plugin.pb.go syntax = "proto3";package proto;message GetRequest { string key = 1;}message GetResponse { bytes value = 1;}message PutRequest { string key = 1; bytes value = 2;}message Empty {}service KV { rpc Get(GetRequest) returns (GetResponse); rpc Put(PutRequest) returns (Empty);} 定义对外暴露的插件接口 新建pkg/shared/plugin.
Read more...

Virtual Kubelet 学习笔记

virtual kubelet 是什么

先看一下阿里巴巴ECI(Elastic Container Instance,弹性容器实例),它为用户提供了一个容器实例,该容器里面可以运行用户服务,说白了就是为你创建一个容器。

ECI适合运行serverless服务,及轻量级微服务。主要应用场景就是,用户可以按照自身业务流量需求在不需要准备服务器,或者虚拟机的情况下,通过ECI来扩容自己的服务。

virtual kubelet 则提供了一种能力,它可以将ECI实例纳管到指定的K8S集群中去,可以通过K8S来管理创建出来的ECI实例。

如何使用virtual kubelet

  • 前提条件,你需要有自己的ECI服务
  • 其次,你需要有一个自己的K8S集群
  • 接下俩,你需要实现virtual kubelet provider接口来扩展你的K8S集群,是它能够将ECI实例纳管到集群中。

开发自己的provider

待续

Read more...

runc 学习

前言 项目开发中需要让一些pod不依赖于docker运行时,也就是说集群节点中docker service停止运行后,业务pod需要正常 运行,此时我们可以使用runc+systemd的方式来启动业务容器。 为了更好的理解runc,先看一下整个docker运行时架构。 docker运行架构 从Docker 1.11开始,Docker容器运行已经不是简单的通过Docker daemon来启动,而是集成了containerd、runC等多个组件,这些组件之间相互协作来完成客户端请求和容器管理。 现在架构图如下: 各个模块交互流程图如下: Docker Daemon Docker Daemon是docker管理容器的守护进程,但是从Docker 1.11开始,守护进程变成了dockerd。 Dockerd对外通过https与Docker client交互,是对容器和镜像操作的上层封装。 Containerd Containerd是容器标准化的产物,当初为了兼容OCI标准,docker公司将容器运行时及容器管理 功能从Docker Daemon中剥离,dockerd只是docker公司在Containerd上的一层封装,Dockerd 通过 Containerd提供的grpc接口 来实现容器管理。 container-shim 是一个真实运行的容器的真实垫片载体,每启动一个容器都会起一个新的container-shim,而后container-shim通过调用runc api(create/delete/kill)来实现容器的增删改查。 为什么需要container-shim呢,是因为用户通过dockerd发送创建或者删除容器请求,containerd收到后需要发送信号给container-shim,由container-shim来调用runc来完成容器创建/删除, 如果containerd直接和runc交互,当删除容器后,runc所启动的容器进程不复存在,无法反馈容器是否完成正确的操作。 其次,通过container-shim可以对接不同的容器运行时,例如kata container。 Runc OCI定义了容器运行时标准,runC是Docker按照开放容器格式标准(OCF, Open Container Format)制定的一种具体实现。 runC是从Docker的libcontainer中迁移而来的,实现了容器启停、资源隔离等功能。Docker默认提供了docker-runc实现,事实上,通过containerd的封装,可以在Docker Daemon启动的时候指定runc的实现。 我们可以通过启动Docker Daemon时增加–add-runtime参数来选择其他的runC现。 docker daemon --add-runtime "custom=/usr/local/bin/my-runc-replacement" 使用Runc 上面我们了解了容器运行时架构,由此发现我们完全可以不依赖于docker,可以直接通过runc来启动容器,并通过systemd来管理runc进程。 假设你的应用已经以pod的形式运行在kubernetes集群中,现在我们使用runc模式来运行你的应用。 创建容器根文件系统 #获取pod中业务容器id $ kubectl describe pod npd #创建npd业务rootfs文件系统 $ mkidr -p npd/rootfs $ cd npd $ docker export podid | tar -C rootfs -xvf - 创建runc spec配置文件config.json $ runc spec $ ls config.json rootfs $ cat config.json 使用runc spec创建出来的配置文件包含了启动一个容器所需要的所有配置信息,但是里面的一些字段需要我们根据自身业务进行补充,内容如下: { "ociVersion": "1.0.0", "process": { "terminal": true, //使用sysytemd启动时需要修改为false,改为非终端模式 "user": { "uid": 0, "gid": 0 }, "args": [ //"sh" //需要替换为你业务启动参数 "/usr/local/bin/eskort-npd", "--v=3", "--detached-mode=detached", .
Read more...

Ceph学习笔记

介绍 ceph 独一无二地用统一的系统提供了三大存储方式: ceph 块存储 ceph 文件存储(cephFS) ceph 对象存储 快速部署 ceph部署方式有很多中,常用方式有使用ceph-deploy来进行部署,这里为了测试使用docker方式快速搭建出一套ceph单机环境出来 docker 部署 $ rm -fr /etc/ceph $ rm -fr /var/lib/ceph $ mkdir -p /etc/ceph $ mkdir -p /var/lib/ceph $ docker run -d --net=host --privileged=true -v /etc/ceph:/etc/ceph -v /var/lib/ceph:/var/lib/ceph -e MON_IP=192.168.0.2 -e CEPH_PUBLIC_NETWORK=192.168.0.1/23 -e CEPH_DEMO_UID=qqq -e CEPH_DEMO_ACCESS_KEY=qqq -e CEPH_DEMO_SECRET_KEY=qqq -e CEPH_DEMO_BUCKET=qqq --name cephdemo ceph/daemon demo $ curl http://192.168.0.0:5000 $ docker exec -it cephdemo ceph -s 架构及组件 1.RADOS (Reliable Automomic Distributed Object Store)是ceph集群的基础,ceph中一切都是对象的形式存储,RADOS负责存储这些对象。确保数据一致性和可靠性。 ceph数据访问方法(如RBD,CephFS,RADOSGW,librados)所有操作方法都是在RADOS之上构建的。 2.OSD ceph对象存储设备,负责存储用户实际数据,一个OSD守护进程和一个物理磁盘绑定,物理磁盘的数量和OSD守护进程的数量是一致的。 ceph osd由一个已经存在的linux文件系统物理磁盘驱动器和OSD服务组成。 ceph OSD 文件系统 -->btrfs或XFS或ext4 物理磁盘 通过命令查看单个节点上OSD的状态: $ service ceph status osd #查看所有osd的ID $ service osd ls #检查osd map的状态 $ ceph osd stat #检查osd 树形图 $ ceph osd tree 3.
Read more...

Calico 学习及源码分析

背景 如何选取合适的网络插件: 1.集群环境因素 如果集群节点机器为虚拟机,不允许机器之间直接二层互联(所谓二层互联也就是说通过交换机可以直接通信,而可以通过交换机直接通信则说明通过交换机互联的机器处于同一个子网中,不同子网直接通信则需要用到路由器), 必须要带有IP地址这种三层的内容才能去做转发,或者限制机器只能使用某些IP等约束,这种情况只能使用overlay模式去实现。常见的有Flannel-vxlan,Calico-ipip,Weave等。 物理机环境同一个子网底层网络可以通过交换机直接通信,此时我们可以使用underlay模式或者路由模式的网络插件。这样避免了使用vxlan封包解包d导致性能降低,常见的插件有Flannel-hostgw,Calico-bgp等。 2.性能因素 pod 创建速度,在pod需要扩容时,overlay模式创建pod速度快,内核接口就可以完成这些操作。underlay模式需要准备底层网络资源,速度较慢。 其次,overlay模式包含封包解包,会带来一些包头的损失、CPU 的消耗等,对网络性能的要求比较高,比如说机器学习、大数据这些场景就不适合使用 Overlay 模式。 calico calico网络模型主要的组件 Felix: 每一个节点上的agent进程,监听etcd,当有新节点加入或者新的容器创建,felix为它设置网卡,IP,MAC,然后更新节点路由表,添加策略到Iptables etcd: 数据存储 BGP Client 每一个节点上都有BGP Client,监听felix写入的路由信息,然后通过BGP协议广播到其他Host节点上去,实现网络互通。该模式比较适合集群规模较小的场景,因为每一个BGP Client需要和集群其他节点互连。集群规模较大时,路由表会被撑满。 BGP Route Reflector: 针对BGP Client存在的问题,对于大规模集群,可以采用BGP的Router Reflector的方法,使所有BGP Client仅与特定Router Reflector节点互联并做路由同步,从而大大减少连接数。 calico 网络模式 calico 作为容器网络方案最大的特点就是它提供了纯三层网络报文转发方案,也就是说每个节点中的容器都可以通过IP地址来直接通信,中间可以通过路由转发找到对方。对比于flannel-hostgw方案,虽然也是通过路由实现通信,但是它依赖于集群所有节点二层连通,然后通过更新每个节点路由的方式实现通信,也就是说flannel-hostgw 方案需要所有节点在同一个子网中,不在同一个子网无法通信。 calico可以实现跨子网通信,是因为他在每一个集群节点中加入了虚拟路由(vrouter),通过BGP路由协议,使得每一个节点中的路由信息可以被传播到其他路由设备,实现三层网络通信。 但是,并不是所有的网络都支持BGP路由协议,calico也提供了overlay模式解决方案—ipip模式 IPIP模式 和其他overlay模式实现的原理一样,ipip模式是在各个节点之间架起一个隧道,使用节点IP封装原始IP报文,启用ipip模式是,calico会在每一个节点上创建一个名字为tunl0的虚拟网络接口。 ping数据包流向图 我们来分析一下一个集群中处于两个不同node的pod数据包是如何传输的,集群网络模式是calico-ipip node2节点ip为172.31.127.252,node2中pod2的ip为10.100.9.206 node1节点ip为172.31.112.2,node1中pod1的ip为10.100.15.150,如下图所示: pod网络分析 首先进入pod1 $ kubectl exec -it pod1 /bin/bash 查看pod1中包含的网络设备,可以看到pod1中只包含lo和eth0 $ ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 4: eth0@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default link/ether ce:83:2b:89:af:9e brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.
Read more...

K8S Proxy 源码分析

前言 在分析kube-proxy之前,我们需要先知道k8s中另外一个概念service。 service 是一组具有相同 label pod 集合的抽象,集群内外的各个服务可以通过 service 进行互相访问。 当创建一个 service 对象时也会对应创建一个 endpoint 对象,endpoint 存放的是pod服务的ip地址,service 只是将多个pod进行关联起来。 当用户通过service访问后端pod服务时,实际的路由转发都是由 kubernetes 中的 kube-proxy 组件来实现,因此,service 必须结合 kube-proxy 使用。 kube-proxy 组件可以运行在 kubernetes 集群中的每一个节点上也可以只运行在单独的几个节点上,其会根据 service 和 endpoints 的变动来刷新节点上 iptables 或者 ipvs 中保存的路由规则。 service 工作原理 endpoint controller 负责监听 service 和对应 pod 的变化,更新对应 service 的 endpoints 对象。当用户创建 service 后 endpoints controller 会监听 pod 的状态,当 pod 处于 running 且准备就绪时,endpoints controller 会将 pod ip 记录到 endpoints 对象中。 kube-proxy 会监听 service 和 endpoints 的变化并调用其代理模块在主机上刷新路由规则。 总结就是,endpoint controller负责更新service ip和其对应后端服务endpoint ip的关系。而kube-proxy则负责更新各个节点的iptables或者ipvs使得流量能够被正常转发。 service 暴露服务方式 ClusterIp ClusterIP 类型的 service 是 kubernetes 集群默认的服务暴露方式,它只能用于集群内部通信,可以被各 pod 访问,其访问方式为: pod ---> ClusterIP:ServicePort --> (iptables)DNAT --> PodIP:containePort NodePort client ---> NodeIP:NodePort ---> ClusterIP:ServicePort ---> (iptables)DNAT ---> PodIP:containePort LoadBalancer Ingress service 数据包流动分析 实验背景就是,我们启动三个nginx pod,pod3和pod2在node2中,而pod1在node1中。网络设备结构图如下: pod2和pod3在同一个node中,为了简洁暂未画出。
Read more...

Kubelet 创建pod流程分析

前言 上一节我们分析了kubelet启动的基本流程,这一节我们分析一下当有新的pod分配到当前节点时,kubelet如何创建一个pod syncLoop 上一节最后我们分析到kl.syncLoop(updates, kl),是kubelet里面一个主循环逻辑,看一下它的具体实现: 它从不同的管道(文件、URL 和 apiserver)监听变化,并把它们汇聚起来。 当有新的变化发生时,它会调用对应的处理函数,保证 pod 处于期望的状态。 如果 pod 没有变化,它也会定期保证所有的容器和最新的期望状态保持一致。这个方法是 for 循环,不会退出。 func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler SyncHandler) { klog.Info("Starting kubelet main sync loop.") syncTicker := time.NewTicker(time.Second) defer syncTicker.Stop() housekeepingTicker := time.NewTicker(housekeepingPeriod) defer housekeepingTicker.Stop() plegCh := kl.pleg.Watch() const ( base = 100 * time.Millisecond max = 5 * time.Second factor = 2 ) duration := base for { if err := kl.runtimeState.runtimeErrors(); err != nil { klog.Infof("skipping pod synchronization - %v", err) // exponential backoff time.Sleep(duration) duration = time.Duration(math.Min(float64(max), factor*float64(duration))) continue } // reset backoff if we have a success duration = base kl.
Read more...

Kube Scheduler 优选策略源码分析

版本环境 kubernetes版本:kubernetes:v1.16.0 go环境:go version go1.13.4 darwin/amd64 前言 预选阶段完成后,进入优选阶段,将需要调度的Pod列表和Node列表传入各种优选算法进行打分,最终整合成结果集HostPriorityList //pkg/scheduler/core/generic_scheduler.go:189 // Schedule tries to schedule the given pod to one of the nodes in the node list. // If it succeeds, it will return the name of the node. // If it fails, it will return a FitError error with reasons. func (g *genericScheduler) Schedule(pod *v1.Pod, pluginContext *framework.PluginContext) (result ScheduleResult, err error) { ... trace.Step("Basic checks done") startPredicateEvalTime := time.Now() filteredNodes, failedPredicateMap, filteredNodesStatuses, err := g.findNodesThatFit(pluginContext, pod) if err != nil { return result, err } // Run "postfilter" plugins. postfilterStatus := g.framework.RunPostFilterPlugins(pluginContext, pod, filteredNodes, filteredNodesStatuses) if !
Read more...

Kubelet 架构分析

版本环境 kubernetes版本:kubernetes:v1.16.0 go环境:go version go1.13.4 darwin/amd64 前言 kubelet是k8s集群中节点的代理人,通过watch api server,来处理发送给本节点的任务,看一下官方对kubelet的描述: The kubelet is the primary "node agent" that runs on each node. It can register the node with the apiserver using one of: the hostname; a flag to override the hostname; or specific logic for a cloud provider. The kubelet works in terms of a PodSpec. A PodSpec is a YAML or JSON object that describes a pod. The kubelet takes a set of PodSpecs that are provided through various mechanisms (primarily through the apiserver) and ensures that the containers described in those PodSpecs are running and healthy.
Read more...

K8S Scheduler 源码分析

版本环境 kubernetes版本:kubernetes:v1.16.0 go环境:go version go1.13.4 darwin/amd64 前言 kube-scheduler 是master节点中重要的组件之一,它同watch接口监听api server中需要调度的pod,然后通过其预选,优选算法来确定pod需要被调度到哪个节点上去。 看一下官方对scheduler 调度流程的描述:The Kubernetes Scheduler 主要步骤如下: 首先通过预选阶段过滤掉不合适的节点,例如根据podSpec中对系统资源的需求,会计算出所有节点剩余的资源是否可以满足pod需求,如果不满足则该节点会被抛弃。 其次通过预选阶段的节点,会再次进行优选。比如在满足pod资源需求的基础上,会优选出当前负载最小节点,保证每个集群节点资源被均衡使用。 最后选择打分最高的节点作为目标节点,如果得分相同则会随机选择一个,进行调度。 For given pod: +---------------------------------------------+ | Schedulable nodes: | | | | +--------+ +--------+ +--------+ | | | node 1 | | node 2 | | node 3 | | | +--------+ +--------+ +--------+ | | | +-------------------+-------------------------+ | | v +-------------------+-------------------------+ Pred. filters: node 3 doesn't have enough resource +-------------------+-------------------------+ | | v +-------------------+-------------------------+ | remaining nodes: | | +--------+ +--------+ | | | node 1 | | node 2 | | | +--------+ +--------+ | | | +-------------------+-------------------------+ | | v +-------------------+-------------------------+ Priority function: node 1: p=2 node 2: p=5 +-------------------+-------------------------+ | | v select max{node priority} = node 2 入口 scheduler使用cobra创建命令行客户端,进入NewSchedulerCommand
Read more...