首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >图解 Deployment Controller 工作流程

图解 Deployment Controller 工作流程

作者头像
CS实验室
发布于 2021-03-22 04:42:08
发布于 2021-03-22 04:42:08
1.2K00
代码可运行
举报
文章被收录于专栏:CS实验室CS实验室
运行总次数:0
代码可运行

本文基于对 Kubernetes v1.16 的源码阅读,通过流程图描述 Deployment Controller 的工作流程,但也包含相关代码以供参考。

上一篇文章《Kubernetes Controller Manager 工作原理》讲解了 Controller Manager 是怎么管理 Controller 的,我们知道 Controller 只需要实现相关事件 handler,无须再关心上层逻辑。本文将基于这篇文章,介绍 Deployment Controller 在接收到来自 Informer 的事件后,做了哪些工作。

Deployment 与控制器模式

在 K8s 中,pod 是最小的资源单位,而 pod 的副本管理是通过 ReplicaSet(RS) 实现的;而 deployment 实则是基于 RS 做了更上层的工作。

这就是 Kubernetes 的控制器模式,顶层资源通过控制下层资源,来拓展新能力。deployment 并没有直接对 pod 进行管理,是通过管理 rs 来实现对 pod 的副本控制。deployment 通过对 rs 的控制实现了版本管理:每次发布对应一个版本,每个版本有一个 rs,在注解中标识版本号,而 rs 再每次根据 pod template 和副本数运行相应的 pod。deployment 只需要保证任何情况下 rs 的状态都在预期,rs 保证任何情况下 pod 的状态都在预期。

K8s 是怎么管理 Deployment 的

了解了 deployment 这一资源在 K8s 中的定位,我们再来看下这个资源如何达到预期状态。

Kubernetes 的 API 和控制器都是基于水平触发的,可以促进系统的自我修复和周期协调。

水平触发这个概念来自硬件的中断,中断可以是水平触发,也可以是边缘触发:

  • 水平触发 : 系统仅依赖于当前状态。即使系统错过了某个事件(可能因为故障挂掉了),当它恢复时,依然可以通过查看信号的当前状态来做出正确的响应。
  • 边缘触发 : 系统不仅依赖于当前状态,还依赖于过去的状态。如果系统错过了某个事件(“边缘”),则必须重新查看该事件才能恢复系统。

Kubernetes 水平触发的 API 实现方式是:控制器监视资源对象的实际状态,并与对象期望的状态进行对比,然后调整实际状态,使之与期望状态相匹配。

水平触发的 API 也叫声明式 API,而监控 deployment 资源对象并确定符合预期的控制器就是 deployment controller,对应的 rs 的控制器就是 rs controller。

Deployment Controller

架构

首先看看 DeploymentController 在 K8s 中的定义:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type DeploymentController struct {
    rsControl     controller.RSControlInterface
    client        clientset.Interface
    eventRecorder record.EventRecorder

    syncHandler func(dKey string) error
    enqueueDeployment func(deployment *apps.Deployment)

    dLister appslisters.DeploymentLister
    rsLister appslisters.ReplicaSetLister
    podLister corelisters.PodLister

    dListerSynced cache.InformerSynced
    rsListerSynced cache.InformerSynced
    podListerSynced cache.InformerSynced

    queue workqueue.RateLimitingInterface
}

主要包括几大块内容:

rsControl 是一个 ReplicaSetController 的工具,用来对 rs 进行认领和弃养工作; client 就是与 APIServer 通信的 client;

eventRecorder 用来记录事件; syncHandler 用来处理 deployment 的同步工作; enqueueDeployment 是一个将 deployment 入 queue 的方法;

dListerrsListerpodLister 分别用来从 shared informer store 中获取 资源的方法; dListerSyncedrsListerSyncedpodListerSynced 分别是用来标识 shared informer store 中是否同步过;

queue 就是 workqueue,deployment、replicaSet、pod 发生变化时,都会将对应的 deployment 推入这个 queue, syncHandler() 方法统一从 workqueue 中处理 deployment。

工作流程

接下来看看 deployment controller 的工作流程。

deployment controller 利用了 informer 的工作能力,实现了资源的监听,同时与其他 controller 协同工作。主要使用了三个 shared informer —— deployment informer、rs informer、pod informer。

首先 deployment controller 会向三个 shared informer 中注册钩子函数,三个钩子函数会在相应事件到来时,将相关的 deployment 推进 workqueue 中。

deployment controller 启动时会有一个 worker 来控制 syncHandler 函数,实时地将 workqueue 中的 item 推出,根据 item 来执行任务。主要包括:领养和弃养 rs、向 eventRecorder 分发事件、根据升级策略决定如何处理下级资源。

workqueue

首先看看 workqueue,这其实是用来辅助 informer 对事件进行分发的队列。整个流程可以梳理为下图。

可以看到,workqueue 分为了三个部分,一是一个先入先出的队列,由切片来实现;还有两个是名为 dirty 和 processing 的 map。整个工作流程分为三个动作,分别为 add、get、done。

add 是将消息推入队列。消息是从 informer 过来的,其实过来的消息并不是事件全部,而是资源 key,即 namespace/name,以这种形式通知业务逻辑有资源的变更事件过来了,需要拿着这个 key 去 indexer 中获取具体资源。如上图绿色的过程所示,在消息进行之后,先检查 dirty 中是否存在,若已存在,不做任何处理,表明事件已经在队列中,不需要重复处理;若 dirty 中不存在,则将该 key 存入 dirty 中(将其作为 map 的键,值为空结构体),再推入队列一份。

get 是 handle 函数从队列中获取 key 的过程。将 key 从队列中 pop 出来的同时,会将其放入 processing 中,并删除其在 dirty 的索引。这一步的原因是将 item 放入 processing 中标记其正在被处理,同时从 dirty 中删除则不影响后面的事件入队列。

done 则是 handle 在处理完 key 之后,必须执行的一步,相当于给 workqueue 发一个 ack,表明我已经处理完毕,该动作仅仅将其从 processing 中删除。

有了这个小而美的先入先出的队列,我们就可以避免资源的多个事件发生时,从 indexer 中重复获取资源的事情发生了。下面来梳理 deployment controller 的具体流程。

replicaSet 的认领和弃养过程

在 deployment controller 的工作流程中,可以注意到除了 deployment 的三个钩子函数,还有 rs 和 pod 的钩子函数,在 rs 的三个钩子函数中,涉及到了 deployment 对 rs 的领养和弃养过程。

rs 认亲

首先来看 rs 的认亲过程:

在 rs 的三个钩子函数中,都会涉及到认亲的过程。

当监听到 rs 的变化后,会根据 rs 的 ownerReferences 字段找到对应的 deployment 入 queue;若该字段为空,意味着这是个孤儿 rs,启动 rs 认亲机制。

认亲过程首先是遍历所有的 deployment,判断 deployment 的 selector 是否与当前 rs 的 labels 相匹配,找到所有与之匹配的 deployment。

然后判断总共有多少个 deployment,若为 0 个,就直接返回,没有人愿意认领,什么都不做,认领过程结束;若大于 1 个,抛错出去,因为这是不正常的行为,不允许多个 deployment 同时拥有同一个 rs;若有且仅有一个 deployment 与之匹配,那么就找到了愿意领养的 deployment,将其入 queue。

addReplicaSet()updateReplicaSet() 的认领过程类似,这里只展示 addReplicaSet() 的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (dc *DeploymentController) addReplicaSet(obj interface{}) {
    rs := obj.(*apps.ReplicaSet)

    if rs.DeletionTimestamp != nil {
        dc.deleteReplicaSet(rs)
        return
    }

    if controllerRef := metav1.GetControllerOf(rs); controllerRef != nil {
        d := dc.resolveControllerRef(rs.Namespace, controllerRef)
        if d == nil {
            return
        }
        klog.V(4).Infof("ReplicaSet %s added.", rs.Name)
        dc.enqueueDeployment(d)
        return
    }

    ds := dc.getDeploymentsForReplicaSet(rs)
    if len(ds) == 0 {
        return
    }
    klog.V(4).Infof("Orphan ReplicaSet %s added.", rs.Name)
    for _, d := range ds {
        dc.enqueueDeployment(d)
    }
}
deployment 领养和弃养

deployment 对 rs 的领养和弃养过程,是发生在从 workqueue 中处理 item 的过程,也是找到当前 deployment 拥有的所有 rs 的过程。

该过程会轮询所有的 rs,如果有 owner 且是当前 deployment,再判断 label 是否满足 deployment 的 selector,满足则列入结果;若不满足,启动弃养机制,仅仅将 rs 的 ownerReferences 删除,使其成为孤儿,不做其他事情。这也是为什么修改了 deployment 的 selector 之后,会多一个 replicas!=0 的 rs 的原因。

如果 rs 没有 owner,是个孤儿,判断 label 是否满足 deployment 的 selector,满足条件,则启动领养机制,将其 ownerReferences 设置为当前 deployment,再列入结果。

下面是整个过程的源码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (m *BaseControllerRefManager) ClaimObject(obj metav1.Object, match func(metav1.Object) bool, adopt, release func(metav1.Object) error) (bool, error) {
    controllerRef := metav1.GetControllerOf(obj)
    if controllerRef != nil {
        if controllerRef.UID != m.Controller.GetUID() {
            return false, nil
        }
        if match(obj) {
            return true, nil
        }

        if m.Controller.GetDeletionTimestamp() != nil {
            return false, nil
        }
        if err := release(obj); err != nil {
            if errors.IsNotFound(err) {
                return false, nil
            }
            return false, err
        }
        return false, nil
    }

    if m.Controller.GetDeletionTimestamp() != nil || !match(obj) {
        return false, nil
    }
    if obj.GetDeletionTimestamp() != nil {
        return false, nil
    }

    if err := adopt(obj); err != nil {
        if errors.IsNotFound(err) {
            return false, nil
        }
        return false, err
    }
    return true, nil
}

领养和弃养的函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (m *ReplicaSetControllerRefManager) AdoptReplicaSet(rs *apps.ReplicaSet) error {
    if err := m.CanAdopt(); err != nil {
        return fmt.Errorf("can't adopt ReplicaSet %v/%v (%v): %v", rs.Namespace, rs.Name, rs.UID, err)
    }

    addControllerPatch := fmt.Sprintf(
        `{"metadata":{"ownerReferences":[{"apiVersion":"%s","kind":"%s","name":"%s","uid":"%s","controller":true,"blockOwnerDeletion":true}],"uid":"%s"}}`,
        m.controllerKind.GroupVersion(), m.controllerKind.Kind,
        m.Controller.GetName(), m.Controller.GetUID(), rs.UID)
    return m.rsControl.PatchReplicaSet(rs.Namespace, rs.Name, []byte(addControllerPatch))
}

func (m *ReplicaSetControllerRefManager) ReleaseReplicaSet(replicaSet *apps.ReplicaSet) error {
    klog.V(2).Infof("patching ReplicaSet %s_%s to remove its controllerRef to %s/%s:%s",
        replicaSet.Namespace, replicaSet.Name, m.controllerKind.GroupVersion(), m.controllerKind.Kind, m.Controller.GetName())
    deleteOwnerRefPatch := fmt.Sprintf(`{"metadata":{"ownerReferences":[{"$patch":"delete","uid":"%s"}],"uid":"%s"}}`, m.Controller.GetUID(), replicaSet.UID)
    err := m.rsControl.PatchReplicaSet(replicaSet.Namespace, replicaSet.Name, []byte(deleteOwnerRefPatch))
    if err != nil {
        if errors.IsNotFound(err) {
            return nil
        }
        if errors.IsInvalid(err) {
            return nil
        }
    }
    return err
}

rolloutRecreate

如果 deployment 的更新策略是 Recreate,其过程是将旧的 pod 删除,再启动新的 pod。具体过程如下:

首先根据上一步 rs 的领养和弃养过程,获得当前 deployment 的所有 rs,排序找出最新的 rs,将其 pod template 与 deployment 的 pod template 比较,若不一致需要创建新的 rs;

创建新的 rs 的过程为:计算当前 deployment 的 pod template 的 hash 值,将其增加至 rs label 及 selector 中;

对所有旧的 rs 计算出最大的 revision,将其加一,作为新 rs 的 revision,为新的 rs 设置如下注解:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
"deployment.kubernetes.io/revision"
"deployment.kubernetes.io/desired-replicas"
"deployment.kubernetes.io/max-replicas"

如果当前 deployment 的 revision 不是最新,将其设为最新;如果需要更新状态,则更新其状态;

将旧的 rs 进行降级,即将其副本数设为 0;

判断当前所有旧的 pod 是否停止,判断条件为 pod 状态为 failed 或 succeed,unknown 或其他所有状态都不是停止状态;若并非所有 pod 都停止了,则退出本次操作,下一个循环再处理;

若所有 pod 都停止了,将新的 rs 进行升级,即将其副本数置为 deployment 的副本数;

最后进行清理工作,比如旧的 rs 数过多时,删除多余的 rs 等。

下面是 rolloutRecreate 的源代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (dc *DeploymentController) rolloutRecreate(d *apps.Deployment, rsList []*apps.ReplicaSet, podMap map[types.UID][]*v1.Pod) error {
    newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false)
    if err != nil {
        return err
    }
    allRSs := append(oldRSs, newRS)
    activeOldRSs := controller.FilterActiveReplicaSets(oldRSs)

    scaledDown, err := dc.scaleDownOldReplicaSetsForRecreate(activeOldRSs, d)
    if err != nil {
        return err
    }
    if scaledDown {
        return dc.syncRolloutStatus(allRSs, newRS, d)
    }

    if oldPodsRunning(newRS, oldRSs, podMap) {
        return dc.syncRolloutStatus(allRSs, newRS, d)
    }

    if newRS == nil {
        newRS, oldRSs, err = dc.getAllReplicaSetsAndSyncRevision(d, rsList, true)
        if err != nil {
            return err
        }
        allRSs = append(oldRSs, newRS)
    }

    if _, err := dc.scaleUpNewReplicaSetForRecreate(newRS, d); err != nil {
        return err
    }

    if util.DeploymentComplete(d, &d.Status) {
        if err := dc.cleanupDeployment(oldRSs, d); err != nil {
            return err
        }
    }

    return dc.syncRolloutStatus(allRSs, newRS, d)
}

rolloutRolling

如果 deployment 的更新策略是 Recreate,其过程是将旧的 pod 删除,再启动新的 pod。具体过程如下:

从上图可以看到,从开始到创建新的 rs 的过程与 rolloutRecreate 过程一致,唯一区别在于,设置新 rs 副本数的过程。在 rolloutRolling 的过程中,新的 rs 的副本数为 deploy.replicas+maxSurge-currentPodCount。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func NewRSNewReplicas(deployment *apps.Deployment, allRSs []*apps.ReplicaSet, newRS *apps.ReplicaSet) (int32, error) {
    switch deployment.Spec.Strategy.Type {
    case apps.RollingUpdateDeploymentStrategyType:
        // Check if we can scale up.
        maxSurge, err := intstrutil.GetValueFromIntOrPercent(deployment.Spec.Strategy.RollingUpdate.MaxSurge, int(*(deployment.Spec.Replicas)), true)
        if err != nil {
            return 0, err
        }
        // Find the total number of pods
        currentPodCount := GetReplicaCountForReplicaSets(allRSs)
        maxTotalPods := *(deployment.Spec.Replicas) + int32(maxSurge)
        if currentPodCount >= maxTotalPods {
            // Cannot scale up.
            return *(newRS.Spec.Replicas), nil
        }
        // Scale up.
        scaleUpCount := maxTotalPods - currentPodCount
        // Do not exceed the number of desired replicas.
        scaleUpCount = int32(integer.IntMin(int(scaleUpCount), int(*(deployment.Spec.Replicas)-*(newRS.Spec.Replicas))))
        return *(newRS.Spec.Replicas) + scaleUpCount, nil
    case apps.RecreateDeploymentStrategyType:
        return *(deployment.Spec.Replicas), nil
    default:
        return 0, fmt.Errorf("deployment type %v isn't supported", deployment.Spec.Strategy.Type)
    }
}

然后到了增减新旧 rs 副本数的过程。主要为先 scale up 新 rs,再 scale down 旧 rs。scale up 新 rs 的过程与上述一致;scale down 旧 rs 的过程为先计算一个最大 scale down 副本数,若小于 0 则不做任何操作;然后在 scale down 的时候做了一个优化,先 scale down 不正常的 rs,可以保证先删除那些不健康的副本;最后如果还有余额,再 scale down 正常的 rs。

每次 scale down 的副本数为 allAvailablePodCount-minAvailable,即 allAvailablePodCount-(deploy.replicas-maxUnavailable)。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (dc *DeploymentController) scaleDownOldReplicaSetsForRollingUpdate(allRSs []*apps.ReplicaSet, oldRSs []*apps.ReplicaSet, deployment *apps.Deployment) (int32, error) {
    maxUnavailable := deploymentutil.MaxUnavailable(*deployment)

    // Check if we can scale down.
    minAvailable := *(deployment.Spec.Replicas) - maxUnavailable
    // Find the number of available pods.
    availablePodCount := deploymentutil.GetAvailableReplicaCountForReplicaSets(allRSs)
    if availablePodCount <= minAvailable {
        // Cannot scale down.
        return 0, nil
    }
    klog.V(4).Infof("Found %d available pods in deployment %s, scaling down old RSes", availablePodCount, deployment.Name)

    sort.Sort(controller.ReplicaSetsByCreationTimestamp(oldRSs))

    totalScaledDown := int32(0)
    totalScaleDownCount := availablePodCount - minAvailable
    for _, targetRS := range oldRSs {
        if totalScaledDown >= totalScaleDownCount {
            // No further scaling required.
            break
        }
        if *(targetRS.Spec.Replicas) == 0 {
            // cannot scale down this ReplicaSet.
            continue
        }
        // Scale down.
        scaleDownCount := int32(integer.IntMin(int(*(targetRS.Spec.Replicas)), int(totalScaleDownCount-totalScaledDown)))
        newReplicasCount := *(targetRS.Spec.Replicas) - scaleDownCount
        if newReplicasCount > *(targetRS.Spec.Replicas) {
            return 0, fmt.Errorf("when scaling down old RS, got invalid request to scale down %s/%s %d -> %d", targetRS.Namespace, targetRS.Name, *(targetRS.Spec.Replicas), newReplicasCount)
        }
        _, _, err := dc.scaleReplicaSetAndRecordEvent(targetRS, newReplicasCount, deployment)
        if err != nil {
            return totalScaledDown, err
        }

        totalScaledDown += scaleDownCount
    }

    return totalScaledDown, nil
}

总结

因为 Kubernetes 采用声明式 API,因此对于 Controller 来说,所做的事情就是根据当前事件计算一个预期的状态,并判断当前实际的状态是否满足预期的状态,如果不满足,则采取行动使其靠拢。

本文通过对 Deployment Controller 的工作流程进行分析,虽然做的事情比较繁琐,但是其所做的事情都是围绕 “向预期靠拢” 这个目标展开的。

希望这两篇文章能让你一窥豹斑,了解 Kubernetes Controller Manager 大致的工作流程和实现方式。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CS实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Docker 私有仓库安装配置 (Registry v2)
使用 Docker Compose + Docker machine 配置一个 Docker 私有仓库。
康怀帅
2018/02/28
2K0
LNMP Docker 安装配置
康怀帅
2017/12/27
1.7K0
Travis CI 构建 GitBook 实践
本文只提供思路,具体实现请查看本人博客的其他文章。务必对 Travis CI 基础知识 了解之后再阅读本文。 刚开始在 Travis CI 中从零开始搭建环境,全部执行时间为 三分半,将环境部署进 Docker, docker run XXX 之后直接开始生成,时间缩短为 一分半。 准备 GitBook 项目文件 新建 .travis 文件夹 复制根目录 book.json 文件 编写 Dockerfile 文件 FROM node:9-alpine ENV TZ=Asia/Shanghai WORKD
康怀帅
2018/02/28
1.1K0
Docker + Drone CI/CD 实践
测试环境:macOS + Drone + Gogs + Docker Registry 生产环境:Debian 9 + Drone + GitHub + 腾讯云容器服务 官方网站:http://drone.io/ GitHub:https://github.com/drone GitHub: https://github.com/khs1994-docker/ci GitHub: https://github.com/khs1994-drone-ci-examples 安装 请使用或升级到最新 0.8 版本
康怀帅
2018/02/28
1.8K0
在生产环境使用 Docker
本文是对官方文档的总结与备注。 官方文档:https://docs.docker.com/engine/userguide/ 配置 Docker 手动启动 Docker 这一部分内容详情可以查看:https://www.khs1994.com/docker/dockerd.html $ sudo docked 自动启动容器 https://docs.docker.com/engine/admin/start-containers-automatically/ $ docker run --restart n
康怀帅
2018/02/28
1.1K0
Docker 远程连接 -- dockerd 命令详解
配置 TLS 实现安全的 Docker 远程连接。 GitHub:https://github.com/khs1994-docker/dockerd-tls 本机:macOS 远程机:使用 VirtualBox 虚拟 CoreOS (IP 192.168.57.110) 目标:能在 macOS 远程操作 CoreOS。(注意不是 SSH 远程登录)。dockerd 命令仅能在 Linux 下使用。 官方文档:https://docs.docker.com/edge/engine/reference/comm
康怀帅
2018/02/28
24.7K2
你必须知道的 17 个 Composer 最佳实践(已更新至 22 个)
尽管大多数 PHP 开发人员都知道如何使用 Composer ,但并不是所有的人都在有效地或以最好的方式使用它。 所以我决定总结一些对我日常工作流程很重要的东西。 大部分技巧的理念是「 Play it safe 」,这意味着如果有更多的方法来处理某些事情,我会使用最不容易出错的方法。
做个快乐的码农
2021/11/29
8.2K0
你必须知道的 17 个 Composer 最佳实践(已更新至 22 个)
WSL 快速搭建 LNMP 环境
康怀帅
2017/12/27
4.6K1
Docker容器化部署,这些最佳实践你不可不知
Docker 作为一种开源的容器化技术,在当今的软件开发和部署领域中发挥着至关重要的作用。它具有诸多显著优势,为开发者和运维人员带来了极大的便利。
天创项目管理分享
2024/11/20
1.4K0
Docker容器化部署,这些最佳实践你不可不知
使用 Docker 安装 Gogs
使用 Docker Compose 安装 Gogs。 GitHub:https://github.com/khs1994-docker/ci docker-compose.yaml 编写 docker-compose.yml 文件 version: '3' services: gogs: image: gogs/gogs ports: - "22:22" - "10080:3000" volumes: - ./data:/data
康怀帅
2018/02/28
2.2K0
基于 Drone + Gogs 构建私有 CI/CD 平台 | Kubernetes 篇
一、基于 Drone + Gogs 构建私有 CI/CD 平台 | Docker 篇
AlicFeng
2021/08/01
1.2K0
基于 Drone + Gogs 构建私有 CI/CD 平台 | Kubernetes 篇
Docker Swarm mode 详解
使用 docker swarm Dcoker 内置的集群管理的工具,Docker CE 1.12+。注意与旧的 Docker Swarm 区分开来。 OS: CoreOS 1562.1.0 3个节点 OS: macOS + Docker Machine Docker Swarm 在 Docker 1.12 版本之前属于一个独立的项目,在 Docker 1.12 版本发布之后,该项目合并到了 Docker 中,成为 Docker 的一个子命令 docker swarm。 有关集群的 Docker 命令如下:
康怀帅
2018/02/28
2.4K0
Docker Compose 项目打包部署
参考官方:https://docs.docker.com/compose/compose-file/
IT茂茂
2020/03/19
5.5K0
基于gitea+drone完成小团队的CI/CD
持续集成和构建的工具有很多,除了著名的 Jenkins,Travis,CircleCI,还有最近比较热门的 Github Action 和 Gitlab CI/CD。但是这些工具面对私人项目不是要收费就是占用大量服务器资源,作为个人开发者的私人项目如果想要使用并不友好。那么开源免费的 Drone CI 是个不错选择,它不但非常轻量,而且十分强大。并可以结合私有代码仓库自动编译、构建服务,几行脚本即可实现自动化部署。本文讲述 Drone CI 的具体实践,结合Gitea,怎么在 VPS 里从零开始搭建一个基于 Gitea + Drone CI 的持续集成系统。
mikelLam
2022/10/31
2.8K0
基于gitea+drone完成小团队的CI/CD
Docker 从入门到实践
来源:Linux学习 ID:LoveLinux1024 一般说来 SPA 的项目我们只要启一个静态文件 Server 就可以了,但是针对传统项目就不一样了,一个项目会依赖很多服务端程序。之前我们的开发模式是在一台开发机上部署开发环境,所有人都在这台开发机上使用 Samba 连接开发。老式开发是没什么问题的,但是前端因为引入了编译流程,增加了 Webpack 打包构建的行为,当多人共同开发的时候经常会因为内存爆满进程被杀导致打包失败。痛定思痛后为了解决这个问题,我决定将 Docker 引入我们的开发环境,通
小小科
2018/06/20
1.2K0
PHP 依赖管理工具 Composer
GitHub:https://github.com/composer/composer 官方网站:https://getcomposer.org/ 开发一个 SDK GitHub:https://github.com/khs1994-php/php-sdk-example { "name": "khs1994/qq-login", "description": "QQ Login SDK", "keywords": [ "qq", "oauth" ], "homepage
康怀帅
2018/02/28
1.1K0
这才是现代PHP该有的样子
标题真的很自恋,不是吗?是啊,就是。虽然我使用了PHP多年,但我怎样陈述出这项工作的最佳实践和工具?我不能我会尝试这统也在不断变化。
猿哥
2019/07/25
1.3K0
基于 Docker 的 Flarum 轻论坛部署方案
Flarum 是一个简洁的轻论坛程序,交互体验做的十分不错,也有良好的插件扩展机制。接触过的人可能知道,它目前还在 beta,在功能更新和迭代方面不算稳定,部署、修改与定制功能更是一件麻烦的事情。
zgq354
2020/06/22
4K0
php应用容器化部署实践
目前市场上 php 仍有一席之地。本文章将探讨如何将 php 应用容器化并迁移部署到 TKE。
谢正伟
2021/05/08
3.6K0
php应用容器化部署实践
看吧,这就是现代化 PHP 该有的样子
这是一篇社区协同翻译的文章,已完成翻译,更多信息请点击 协同翻译介绍 。 讨论请前往:https://laravel-china.org/topics/8690 文章的标题真是自命不凡,不是吗?是的,虽然我们使用 PHP 工作很多年,但是我们能够说出哪些是最佳实践和最好的工具吗?我不能,但是我将要去这么做。 我看到开发者们使用 PHP 工作的方式正在发生真正的变化,不仅因为 PHP 新的版本和自身逐步的完善,让 PHP 语言发生了巨大变化,变得更加成熟和健壮,更重要的是整个生态系统也在不断地改变。 为了
前端教程
2018/03/27
1.6K0
看吧,这就是现代化 PHP 该有的样子
相关推荐
Docker 私有仓库安装配置 (Registry v2)
更多 >
交个朋友
加入架构与运维学习入门群
系统架构设计入门 运维体系构建指南
加入架构与运维工作实战群
高并发系统设计 运维自动化实践
加入[架构及运维] 腾讯云技术交流站
云架构设计 云运维最佳实践
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档