资讯专栏INFORMATION COLUMN

kube-proxy源码解析

X1nFLY / 2970人阅读

摘要:广告离线安装包,仅需三步源码解析相对于模式具备较高的性能与稳定性本文讲以此模式的源码解析为主,如果想去了解模式的原理,可以去参考其实现,架构上无差别。主要功能是监听和的事件,然后下放代理策略到机器上。

</>复制代码

  1. 广告 | kubernetes离线安装包,仅需三步
kube-proxy源码解析

ipvs相对于iptables模式具备较高的性能与稳定性, 本文讲以此模式的源码解析为主,如果想去了解iptables模式的原理,可以去参考其实现,架构上无差别。

kube-proxy主要功能是监听service和endpoint的事件,然后下放代理策略到机器上。 底层调用docker/libnetwork, 而libnetwork最终调用了netlink 与netns来实现ipvs的创建等动作

初始化配置

代码入口:cmd/kube-proxy/app/server.go Run() 函数

通过命令行参数去初始化proxyServer的配置

</>复制代码

  1. proxyServer, err := NewProxyServer(o)

</>复制代码

  1. type ProxyServer struct {
  2. // k8s client
  3. Client clientset.Interface
  4. EventClient v1core.EventsGetter
  5. // ipvs 相关接口
  6. IptInterface utiliptables.Interface
  7. IpvsInterface utilipvs.Interface
  8. IpsetInterface utilipset.Interface
  9. // 处理同步时的处理器
  10. Proxier proxy.ProxyProvider
  11. // 代理模式,ipvs iptables userspace kernelspace(windows)四种
  12. ProxyMode string
  13. // 配置同步周期
  14. ConfigSyncPeriod time.Duration
  15. // service 与 endpoint 事件处理器
  16. ServiceEventHandler config.ServiceHandler
  17. EndpointsEventHandler config.EndpointsHandler
  18. }

Proxier是主要入口,抽象了两个函数:

</>复制代码

  1. type ProxyProvider interface {
  2. // Sync immediately synchronizes the ProxyProvider"s current state to iptables.
  3. Sync()
  4. // 定期执行
  5. SyncLoop()
  6. }

ipvs 的interface 这个很重要:

</>复制代码

  1. type Interface interface {
  2. // 删除所有规则
  3. Flush() error
  4. // 增加一个virtual server
  5. AddVirtualServer(*VirtualServer) error
  6. UpdateVirtualServer(*VirtualServer) error
  7. DeleteVirtualServer(*VirtualServer) error
  8. GetVirtualServer(*VirtualServer) (*VirtualServer, error)
  9. GetVirtualServers() ([]*VirtualServer, error)
  10. // 给virtual server加个realserver, 如 VirtualServer就是一个clusterip realServer就是pod(或者自定义的endpoint)
  11. AddRealServer(*VirtualServer, *RealServer) error
  12. GetRealServers(*VirtualServer) ([]*RealServer, error)
  13. DeleteRealServer(*VirtualServer, *RealServer) error
  14. }

我们在下文再详细看ipvs_linux是如何实现上面接口的

virtual server与realserver, 最重要的是ip:port,然后就是一些代理的模式如sessionAffinity等:

</>复制代码

  1. type VirtualServer struct {
  2. Address net.IP
  3. Protocol string
  4. Port uint16
  5. Scheduler string
  6. Flags ServiceFlags
  7. Timeout uint32
  8. }
  9. type RealServer struct {
  10. Address net.IP
  11. Port uint16
  12. Weight int
  13. }

</>复制代码

  1. 创建apiserver client

</>复制代码

  1. client, eventClient, err := createClients(config.ClientConnection, master)

</>复制代码

  1. 创建Proxier 这是仅仅关注ipvs模式的proxier

</>复制代码

  1. else if proxyMode == proxyModeIPVS {
  2. glog.V(0).Info("Using ipvs Proxier.")
  3. proxierIPVS, err := ipvs.NewProxier(
  4. iptInterface,
  5. ipvsInterface,
  6. ipsetInterface,
  7. utilsysctl.New(),
  8. execer,
  9. config.IPVS.SyncPeriod.Duration,
  10. config.IPVS.MinSyncPeriod.Duration,
  11. config.IPTables.MasqueradeAll,
  12. int(*config.IPTables.MasqueradeBit),
  13. config.ClusterCIDR,
  14. hostname,
  15. getNodeIP(client, hostname),
  16. recorder,
  17. healthzServer,
  18. config.IPVS.Scheduler,
  19. )
  20. ...
  21. proxier = proxierIPVS
  22. serviceEventHandler = proxierIPVS
  23. endpointsEventHandler = proxierIPVS

这个Proxier具备以下方法:

</>复制代码

  1. +OnEndpointsAdd(endpoints *api.Endpoints)
  2. +OnEndpointsDelete(endpoints *api.Endpoints)
  3. +OnEndpointsSynced()
  4. +OnEndpointsUpdate(oldEndpoints, endpoints *api.Endpoints)
  5. +OnServiceAdd(service *api.Service)
  6. +OnServiceDelete(service *api.Service)
  7. +OnServiceSynced()
  8. +OnServiceUpdate(oldService, service *api.Service)
  9. +Sync()
  10. +SyncLoop()

所以ipvs的这个Proxier实现了我们需要的绝大部分接口

小结一下:

</>复制代码

  1. +-----------> endpointHandler
  2. |
  3. +-----------> serviceHandler
  4. | ^
  5. | | +-------------> sync 定期同步等
  6. | | |
  7. ProxyServer---------> Proxier --------> service 事件回调
  8. | |
  9. | +-------------> endpoint事件回调
  10. | | 触发
  11. +-----> ipvs interface ipvs handler <-----+
启动proxyServer

检查是不是带了clean up参数,如果带了那么清除所有规则退出

OOM adjuster貌似没实现,忽略

resouceContainer也没实现,忽略

启动metrics服务器,这个挺重要,比如我们想监控时可以传入这个参数, 包含promethus的 metrics. metrics-bind-address参数

启动informer, 开始监听事件,分别启动协程处理。

1 2 3 4我们都不用太关注,细看5即可:

</>复制代码

  1. informerFactory := informers.NewSharedInformerFactory(s.Client, s.ConfigSyncPeriod)
  2. serviceConfig := config.NewServiceConfig(informerFactory.Core().InternalVersion().Services(), s.ConfigSyncPeriod)
  3. // 注册 service handler并启动
  4. serviceConfig.RegisterEventHandler(s.ServiceEventHandler)
  5. // 这里面仅仅是把ServiceEventHandler赋值给informer回调
  6. go serviceConfig.Run(wait.NeverStop)
  7. endpointsConfig := config.NewEndpointsConfig(informerFactory.Core().InternalVersion().Endpoints(), s.ConfigSyncPeriod)
  8. // 注册endpoint
  9. endpointsConfig.RegisterEventHandler(s.EndpointsEventHandler)
  10. go endpointsConfig.Run(wait.NeverStop)
  11. go informerFactory.Start(wait.NeverStop)

serviceConfig.Run与endpointConfig.Run仅仅是给回调函数赋值, 所以注册的handler就给了informer, informer监听到事件时就会回调:

</>复制代码

  1. for i := range c.eventHandlers {
  2. glog.V(3).Infof("Calling handler.OnServiceSynced()")
  3. c.eventHandlers[i].OnServiceSynced()
  4. }

那么问题来了,注册进去的这个handler是啥? 回顾一下上文的

</>复制代码

  1. serviceEventHandler = proxierIPVS
  2. endpointsEventHandler = proxierIPVS

所以都是这个proxierIPVS

handler的回调函数, informer会回调这几个函数,所以我们在自己开发时实现这个interface注册进去即可:

</>复制代码

  1. type ServiceHandler interface {
  2. // OnServiceAdd is called whenever creation of new service object
  3. // is observed.
  4. OnServiceAdd(service *api.Service)
  5. // OnServiceUpdate is called whenever modification of an existing
  6. // service object is observed.
  7. OnServiceUpdate(oldService, service *api.Service)
  8. // OnServiceDelete is called whenever deletion of an existing service
  9. // object is observed.
  10. OnServiceDelete(service *api.Service)
  11. // OnServiceSynced is called once all the initial even handlers were
  12. // called and the state is fully propagated to local cache.
  13. OnServiceSynced()
  14. }
开始监听

</>复制代码

  1. go informerFactory.Start(wait.NeverStop)

这里执行后,我们创建删除service endpoint等动作都会被监听到,然后回调,回顾一下上面的图,最终都是由Proxier去实现,所以后面我们重点关注Proxier即可

</>复制代码

  1. s.Proxier.SyncLoop()

然后开始SyncLoop,下文开讲

Proxier 实现

我们创建一个service时OnServiceAdd方法会被调用, 这里记录一下之前的状态与当前状态两个东西,然后发个信号给syncRunner让它去处理:

</>复制代码

  1. func (proxier *Proxier) OnServiceAdd(service *api.Service) {
  2. namespacedName := types.NamespacedName{Namespace: service.Namespace, Name: service.Name}
  3. if proxier.serviceChanges.update(&namespacedName, nil, service) && proxier.isInitialized() {
  4. proxier.syncRunner.Run()
  5. }
  6. }

记录service 信息,可以看到没做什么事,就是把service存在map里, 如果没变直接删掉map信息不做任何处理:

</>复制代码

  1. change, exists := scm.items[*namespacedName]
  2. if !exists {
  3. change = &serviceChange{}
  4. // 老的service信息
  5. change.previous = serviceToServiceMap(previous)
  6. scm.items[*namespacedName] = change
  7. }
  8. // 当前监听到的service信息
  9. change.current = serviceToServiceMap(current)
  10. 如果一样,直接删除
  11. if reflect.DeepEqual(change.previous, change.current) {
  12. delete(scm.items, *namespacedName)
  13. }

proxier.syncRunner.Run() 里面就发送了一个信号

</>复制代码

  1. select {
  2. case bfr.run <- struct{}{}:
  3. default:
  4. }

这里面处理了这个信号

</>复制代码

  1. s.Proxier.SyncLoop()
  2. func (proxier *Proxier) SyncLoop() {
  3. // Update healthz timestamp at beginning in case Sync() never succeeds.
  4. if proxier.healthzServer != nil {
  5. proxier.healthzServer.UpdateTimestamp()
  6. }
  7. proxier.syncRunner.Loop(wait.NeverStop)
  8. }

runner里收到信号执行,没收到信号会定期执行:

</>复制代码

  1. func (bfr *BoundedFrequencyRunner) Loop(stop <-chan struct{}) {
  2. glog.V(3).Infof("%s Loop running", bfr.name)
  3. bfr.timer.Reset(bfr.maxInterval)
  4. for {
  5. select {
  6. case <-stop:
  7. bfr.stop()
  8. glog.V(3).Infof("%s Loop stopping", bfr.name)
  9. return
  10. case <-bfr.timer.C(): // 定期执行
  11. bfr.tryRun()
  12. case <-bfr.run:
  13. bfr.tryRun() // 收到事件信号执行
  14. }
  15. }
  16. }

这个bfr runner里我们最需要主意的是一个回调函数,tryRun里检查这个回调是否满足被调度的条件:

</>复制代码

  1. type BoundedFrequencyRunner struct {
  2. name string // the name of this instance
  3. minInterval time.Duration // the min time between runs, modulo bursts
  4. maxInterval time.Duration // the max time between runs
  5. run chan struct{} // try an async run
  6. mu sync.Mutex // guards runs of fn and all mutations
  7. fn func() // function to run, 这个回调
  8. lastRun time.Time // time of last run
  9. timer timer // timer for deferred runs
  10. limiter rateLimiter // rate limiter for on-demand runs
  11. }
  12. // 传入的proxier.syncProxyRules这个函数
  13. proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner", proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)

这是个600行左右的搓逼函数,也是处理主要逻辑的地方。

syncProxyRules

设置一些iptables规则,如mark与comment

确定机器上有网卡,ipvs需要绑定地址到上面

确定有ipset,ipset是iptables的扩展,可以给一批地址设置iptables规则

...(又臭又长,重复代码多,看不下去了,细节问题自己去看吧)

我们最关注的,如何去处理VirtualServer的

</>复制代码

  1. serv := &utilipvs.VirtualServer{
  2. Address: net.ParseIP(ingress.IP),
  3. Port: uint16(svcInfo.port),
  4. Protocol: string(svcInfo.protocol),
  5. Scheduler: proxier.ipvsScheduler,
  6. }
  7. if err := proxier.syncService(svcNameString, serv, false); err == nil {
  8. if err := proxier.syncEndpoint(svcName, svcInfo.onlyNodeLocalEndpoints, serv); err != nil {
  9. }
  10. }

看下实现, 如果没有就创建,如果已存在就更新, 给网卡绑定service的cluster ip:

</>复制代码

  1. func (proxier *Proxier) syncService(svcName string, vs *utilipvs.VirtualServer, bindAddr bool) error {
  2. appliedVirtualServer, _ := proxier.ipvs.GetVirtualServer(vs)
  3. if appliedVirtualServer == nil || !appliedVirtualServer.Equal(vs) {
  4. if appliedVirtualServer == nil {
  5. if err := proxier.ipvs.AddVirtualServer(vs); err != nil {
  6. return err
  7. }
  8. } else {
  9. if err := proxier.ipvs.UpdateVirtualServer(appliedVirtualServer); err != nil {
  10. return err
  11. }
  12. }
  13. }
  14. // bind service address to dummy interface even if service not changed,
  15. // in case that service IP was removed by other processes
  16. if bindAddr {
  17. _, err := proxier.netlinkHandle.EnsureAddressBind(vs.Address.String(), DefaultDummyDevice)
  18. if err != nil {
  19. return err
  20. }
  21. }
  22. return nil
  23. }
创建service实现

现在可以去看ipvs的AddVirtualServer的实现了,主要是利用socket与内核进程通信做到的。
pkg/util/ipvs/ipvs_linux.go 里 runner结构体实现了这些方法, 这里用到了 docker/libnetwork/ipvs库:

</>复制代码

  1. // runner implements Interface.
  2. type runner struct {
  3. exec utilexec.Interface
  4. ipvsHandle *ipvs.Handle
  5. }
  6. // New returns a new Interface which will call ipvs APIs.
  7. func New(exec utilexec.Interface) Interface {
  8. ihandle, err := ipvs.New("") // github.com/docker/libnetwork/ipvs
  9. if err != nil {
  10. glog.Errorf("IPVS interface can"t be initialized, error: %v", err)
  11. return nil
  12. }
  13. return &runner{
  14. exec: exec,
  15. ipvsHandle: ihandle,
  16. }
  17. }

New的时候创建了一个特殊的socket, 这里与我们普通的socket编程无差别,关键是syscall.AF_NETLINK这个参数,代表与内核进程通信:

</>复制代码

  1. sock, err := nl.GetNetlinkSocketAt(n, netns.None(), syscall.NETLINK_GENERIC)
  2. func getNetlinkSocket(protocol int) (*NetlinkSocket, error) {
  3. fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW|syscall.SOCK_CLOEXEC, protocol)
  4. if err != nil {
  5. return nil, err
  6. }
  7. s := &NetlinkSocket{
  8. fd: int32(fd),
  9. }
  10. s.lsa.Family = syscall.AF_NETLINK
  11. if err := syscall.Bind(fd, &s.lsa); err != nil {
  12. syscall.Close(fd)
  13. return nil, err
  14. }
  15. return s, nil
  16. }

创建一个service, 转换成docker service格式,直接调用:

</>复制代码

  1. // AddVirtualServer is part of Interface.
  2. func (runner *runner) AddVirtualServer(vs *VirtualServer) error {
  3. eSvc, err := toBackendService(vs)
  4. if err != nil {
  5. return err
  6. }
  7. return runner.ipvsHandle.NewService(eSvc)
  8. }

然后就是把service信息打包,往socket里面写即可:

</>复制代码

  1. func (i *Handle) doCmdwithResponse(s *Service, d *Destination, cmd uint8) ([][]byte, error) {
  2. req := newIPVSRequest(cmd)
  3. req.Seq = atomic.AddUint32(&i.seq, 1)
  4. if s == nil {
  5. req.Flags |= syscall.NLM_F_DUMP //Flag to dump all messages
  6. req.AddData(nl.NewRtAttr(ipvsCmdAttrService, nil)) //Add a dummy attribute
  7. } else {
  8. req.AddData(fillService(s))
  9. } // 把service塞到请求中
  10. if d == nil {
  11. if cmd == ipvsCmdGetDest {
  12. req.Flags |= syscall.NLM_F_DUMP
  13. }
  14. } else {
  15. req.AddData(fillDestinaton(d))
  16. }
  17. // 给内核进程发送service信息
  18. res, err := execute(i.sock, req, 0)
  19. if err != nil {
  20. return [][]byte{}, err
  21. }
  22. return res, nil
  23. }

</>复制代码

  1. 构造请求

</>复制代码

  1. func newIPVSRequest(cmd uint8) *nl.NetlinkRequest {
  2. return newGenlRequest(ipvsFamily, cmd)
  3. }

在构造请求时传入的是ipvs协议簇

然后构造一个与内核通信的消息头

</>复制代码

  1. func NewNetlinkRequest(proto, flags int) *NetlinkRequest {
  2. return &NetlinkRequest{
  3. NlMsghdr: syscall.NlMsghdr{
  4. Len: uint32(syscall.SizeofNlMsghdr),
  5. Type: uint16(proto),
  6. Flags: syscall.NLM_F_REQUEST | uint16(flags),
  7. Seq: atomic.AddUint32(&nextSeqNr, 1),
  8. },
  9. }
  10. }

</>复制代码

  1. 给消息加Data,这个Data是个数组,需要实现两个方法:

</>复制代码

  1. type NetlinkRequestData interface {
  2. Len() int // 长度
  3. Serialize() []byte // 序列化, 内核通信也需要一定的数据格式,service信息也需要实现
  4. }

比如 header是这样序列化的, 一看愣住了,思考好久才看懂:
拆下看:
([unsafe.Sizeof(hdr)]byte) 一个*[]byte类型,长度就是结构体大小
(unsafe.Pointer(hdr))把结构体转成byte指针类型
加个*取它的值
用[:]转成byte返回

</>复制代码

  1. func (hdr *genlMsgHdr) Serialize() []byte {
  2. return (*(*[unsafe.Sizeof(*hdr)]byte)(unsafe.Pointer(hdr)))[:]
  3. }

</>复制代码

  1. 发送service信息给内核

一个很普通的socket发送接收数据

</>复制代码

  1. func execute(s *nl.NetlinkSocket, req *nl.NetlinkRequest, resType uint16) ([][]byte, error) {
  2. var (
  3. err error
  4. )
  5. if err := s.Send(req); err != nil {
  6. return nil, err
  7. }
  8. pid, err := s.GetPid()
  9. if err != nil {
  10. return nil, err
  11. }
  12. var res [][]byte
  13. done:
  14. for {
  15. msgs, err := s.Receive()
  16. if err != nil {
  17. return nil, err
  18. }
  19. for _, m := range msgs {
  20. if m.Header.Seq != req.Seq {
  21. continue
  22. }
  23. if m.Header.Pid != pid {
  24. return nil, fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid)
  25. }
  26. if m.Header.Type == syscall.NLMSG_DONE {
  27. break done
  28. }
  29. if m.Header.Type == syscall.NLMSG_ERROR {
  30. error := int32(native.Uint32(m.Data[0:4]))
  31. if error == 0 {
  32. break done
  33. }
  34. return nil, syscall.Errno(-error)
  35. }
  36. if resType != 0 && m.Header.Type != resType {
  37. continue
  38. }
  39. res = append(res, m.Data)
  40. if m.Header.Flags&syscall.NLM_F_MULTI == 0 {
  41. break done
  42. }
  43. }
  44. }
  45. return res, nil
  46. }

</>复制代码

  1. Service 数据打包
    这里比较细,核心思想就是内核只认一定格式的标准数据,我们把service信息按其标准打包发送给内核即可。
    至于怎么打包的就不详细讲了。

</>复制代码

  1. func fillService(s *Service) nl.NetlinkRequestData {
  2. cmdAttr := nl.NewRtAttr(ipvsCmdAttrService, nil)
  3. nl.NewRtAttrChild(cmdAttr, ipvsSvcAttrAddressFamily, nl.Uint16Attr(s.AddressFamily))
  4. if s.FWMark != 0 {
  5. nl.NewRtAttrChild(cmdAttr, ipvsSvcAttrFWMark, nl.Uint32Attr(s.FWMark))
  6. } else {
  7. nl.NewRtAttrChild(cmdAttr, ipvsSvcAttrProtocol, nl.Uint16Attr(s.Protocol))
  8. nl.NewRtAttrChild(cmdAttr, ipvsSvcAttrAddress, rawIPData(s.Address))
  9. // Port needs to be in network byte order.
  10. portBuf := new(bytes.Buffer)
  11. binary.Write(portBuf, binary.BigEndian, s.Port)
  12. nl.NewRtAttrChild(cmdAttr, ipvsSvcAttrPort, portBuf.Bytes())
  13. }
  14. nl.NewRtAttrChild(cmdAttr, ipvsSvcAttrSchedName, nl.ZeroTerminated(s.SchedName))
  15. if s.PEName != "" {
  16. nl.NewRtAttrChild(cmdAttr, ipvsSvcAttrPEName, nl.ZeroTerminated(s.PEName))
  17. }
  18. f := &ipvsFlags{
  19. flags: s.Flags,
  20. mask: 0xFFFFFFFF,
  21. }
  22. nl.NewRtAttrChild(cmdAttr, ipvsSvcAttrFlags, f.Serialize())
  23. nl.NewRtAttrChild(cmdAttr, ipvsSvcAttrTimeout, nl.Uint32Attr(s.Timeout))
  24. nl.NewRtAttrChild(cmdAttr, ipvsSvcAttrNetmask, nl.Uint32Attr(s.Netmask))
  25. return cmdAttr
  26. }
总结

Service总体来讲代码比较简单,但是觉得有些地方实现的有点绕,不够简单直接。 总体来说就是监听apiserver事件,然后比对 处理,定期也会去执行同步策略.

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/32669.html

相关文章

  • kube-proxy源码解析

    摘要:广告离线安装包,仅需三步源码解析相对于模式具备较高的性能与稳定性本文讲以此模式的源码解析为主,如果想去了解模式的原理,可以去参考其实现,架构上无差别。主要功能是监听和的事件,然后下放代理策略到机器上。 广告 | kubernetes离线安装包,仅需三步 kube-proxy源码解析 ipvs相对于iptables模式具备较高的性能与稳定性, 本文讲以此模式的源码解析为主,如果想去了解...

    thursday 评论0 收藏0
  • 循序渐进的手动安装k8s笔记-2

    摘要:启动并设置为开机自启动安装服务这部分配置与上一篇笔记完全相同。我们创建这个文件并填入如下内容安装完和之后将其启动并设置为开机自启动以上,角色的功能已经安装完成。 上一篇笔记中,我尝试了使用 k8s 1.6 版本安装一个最简单的集群。这一次,我希望能够增加 node 的数量并且安装网络插件,然后配置内部的域名解析功能。 在起初的设想中,我仍然希望不配置各个组件间的认证,只关心功能的正常运...

    pingink 评论0 收藏0
  • 浅析k8s service的应用

    摘要:前言最近在产品新版本的服务发现和负载均衡方案上遇到了一个问题,在尽量不改动原生使用方式和代码前提下,对又重新复习了一遍,略有体会。所有访问该的请求,都会被转发到后端的中。使用这种方案的原因,不外乎是外部无法访问容器服务。 前言 最近在产品新版本的服务发现和负载均衡方案上遇到了一个问题,在尽量不改动原生k8s使用方式和代码前提下,对service又重新复习了一遍,略有体会。 Servic...

    Clect 评论0 收藏0
  • 拥抱云原生,基于eBPF技术实现Serverless节点访问K8S Service

    摘要:但事实是,并不完美,甚至存在严重的问题。容器产品拥抱正在改变云原生生态,未来容器云产品与容器产品将紧密结合业内最新进展,挖掘在网络,负载均衡,监控等领域的应用,为用户提供更好的观测定位和调优能力。Serverless容器的服务发现2020年9月,UCloud上线了Serverless容器产品Cube,它具备了虚拟机级别的安全隔离、轻量化的系统占用、秒级的启动速度,高度自动化的弹性伸缩,以及简...

    Tecode 评论0 收藏0
  • #yyds干货盘点#k8s Service 资源及其模型

    摘要:每个工作节点的组件通过持续监控着各及其关联的对象,并将对象的创建或变动实时反映至当前工作节点上相应的或规则上。资源都可统一根据其工作逻辑分为和这种类型。 Service 是 Kubernetes 的核心资源类型之一,通常被看作微服务的一种实现。它事实上是一种抽象:通过规则定义出由多个 Pod 对象组合而成的逻辑集合,以及访...

    silencezwm 评论0 收藏0

发表评论

0条评论

X1nFLY

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<