精华内容
下载资源
问答
  • 本来想使用自建的K8S集群来演示混合部署的,无奈作者的电脑配置实在是跟不上,一开虚拟机就卡得不行。于是果断选择氪金使用阿里云的容器服务K8S版,既省去了自己各种操作的压力,也权当是体验一把国内顶级的...

    本来想使用自建的K8S集群来演示混合部署的,无奈作者的电脑配置实在是跟不上,一开虚拟机就卡得不行。于是果断选择氪金使用阿里云的容器服务K8S版,既省去了自己各种操作的压力,也权当是体验一把国内顶级的Kubernetes容器服务。
    在这里插入图片描述

    一、混合部署的概念

    在开始介绍混合部署的概念之前,我们需要先考虑K8S在实际业务中可能存在的问题。

    假设你是一个中小企业的运维,并且公司业务都部署在K8S的集群上。那么毫无疑问,我们要尽可能地发挥K8S集群的机器利用率,也就是尽可能地不浪费K8S集群的计算能力。因为如果你不充分利用,一看CPU使用率连5%都不到,那一堆设备就相当于放那吃灰,这对于企业来说显然是不能接受的。

    我们假设业务分为在线和离线两种模式:在线任务需要资源相对较少,但要求响应时间短;离线任务则不需要对任务进行迅速响应,但是计算量相对较大、占用资源多。那么我们应该怎么在K8S集群上完成这两种业务的部署、并且使得CPU利用率始终维持在一个比较高的水平上呢?

    答案显而易见,就是“混合部署”,即在一台计算机上同时部署咱们的在线任务和离线任务。(当然,混合部署更常见的说法是混合云部署)

    那么这么搞,问题也随之而来了。比方说我们的在线服务是给客户端提供Web资源,但是某天离线任务(比如TF)突然来了好几十个TB的数据要训练,一下就把所有node资源给占用了。那这下可好了,用户点我们的网站进不去,一天损失好几亿,一下就把我们之前辛辛苦苦省下来的买服务器的钱给霍霍干净了,这不完犊子了吗?

    不要慌,这个问题早就得到了解决。接下来,咱们就来学习一下Kubernetes中的QoS管理。

    二、QoS相关知识

    QoS(Quality of Service),大部分译为 “服务质量等级”,又译作 “服务质量保证”,是作用在 Pod 上的一个配置,当 Kubernetes 创建一个 Pod 时,它就会给这个 Pod 分配一个 QoS 等级。

    我们使用的QoS等级主要有以下三个,他们所决定Pod的重要程度从上到下依次递减。

    • Guaranteed:Pod 里的每个容器都必须有内存/CPU 限制和请求,而且值必须相等。如果一个容器只指明limit而未设定request,则request的值等于limit值,是最严格的要求。
    • Burstable:Pod 里至少有一个容器有内存或者 CPU 请求且不满足 Guarantee 等级的要求,即内存/CPU 的值设置的不同。或者可以这么表达:在不满足Guaranteed的情况下,至少设置一个CPU或者内存的请求。
    • BestEffort:容器必须没有任何内存或者 CPU 的限制或请求,就很佛系。

    在这里插入图片描述
    那么QoS的作用体现在哪呢?我们举个例子,比方说计算机中的内存和磁盘资源是不可以被压缩的,而当资源紧俏时,kubelet会根据资源对象的QoS进行驱逐:

    • Guaranteed,最高优先级,最后kill。除非超过limit或者没有其他低优先级的Pod。
    • Burstable,第二个被kill。
    • BestEffort,最低优先级,第一个被kill。

    这也就意味着如果一个Node中的内存和磁盘资源被多个Pod给抢占了,那么QoS为BestEffort的Pod显然是第一个被kill的。那么说到这里,想必各位读者对于上一节中存在的问题应该如何解决想必已经有了答案。我们在实际部署的时候,可以把在线服务所使用的Pod的QoS设置为Guaranteed,这样就避免了负载过大的离线任务占用过多资源把我们的在线Pod给挤掉的情况。

    哔哔了这么多,咱们还是看几个Pod的yaml文件来学习一下QoS。我们创建一个QoS为Guaranteed的Pod。

    # kubectl create namespace example-qos
    

    QoS为Guaranteed的pod中每个容器都必须包含内存和CPU的请求和限制,并且值相等。那么我们就遵循这一原则,编写如下的yaml文件。

    apiVersion: v1
    kind: Pod
    metadata:
      name: qos-demo
      namespace: example-qos
    spec:
      containers:
      - name: qos-demo-ctr
        image: nginx
        resources:
          limits:
            memory: "300Mi"
            cpu: "500m"
          requests:
            memory: "300Mi"
            cpu: "500m"
    

    接下来我们启动该pod,并查看该pod的详细信息。

    # kubectl apply -f https://k8s.io/examples/pods/qos/qos-pod.yaml --namespace=example-qos
    # kubectl get pod qos-demo --namespace=example-qos --output=yaml
    

    在这里插入图片描述
    如果正常的话,最后会多出一行qosClass: Guaranteed,这代表我们创建的Pod的QoS为Guaranteed。如果Pod仅声明了内存限制,而没声明内存请求,那么kubernetes会自动赋予它与限制相同的内存请求。CPU也是如此。

    三、操作过程

    因为作者是第一次体验阿里云K8S容器服务,除了跟着阿里手册和K8S中文社区,还借鉴了不少技术文档,因此在步骤上可能稍有出入。以下步骤仅供参考,具体请以官方文档为准。

    我们先登录阿里云的容器服务K8S的控制台

    1. 创建Kubernetes集群

    想要创建一个Kubernetes集群,需要开通容器服务、弹性伸缩(ESS)服务和访问控制(RAM)服务。因此我们先登录容器服务管理控制台 、RAM 管理控制台和弹性伸缩控制台开通相应的服务。

    控制台界面如下所示。
    在这里插入图片描述
    我们点击创建集群,进入以下界面。
    在这里插入图片描述由于阿里的官方文档已经很详细地解释了每一步的各个选项的内容,这里本人就不狗尾续貂了,仅简单介绍阿里云K8S对应的三种服务模式。

    容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的云原生应用管理能力,并提供了以下三种 Kubernetes 集群形态,

    • 专有版 Kubernetes: 需要创建 3 个 Master(高可用)节点及若干 Worker 节点,可对集群基础设施进行更细粒度的控制,需要自行规划、维护、升级服务器集群。
    • 托管版 Kubernetes:只需创建 Worker 节点,Master 节点由容器服务创建并托管。具备简单、低成本、高可用、无需运维管理 Kubernetes 集群 Master 节点的特点,您可以更多关注业务本身。
    • Serverless Kubernetes:无需创建和管理 Master 节点及 Worker 节点,即可通过控制台或者命令配置容器实例的资源、指明应用容器镜像以及对外服务的方式,直接启动应用程序。

    那么为了体验原生Kubernetes的操作,我们这里选择的是专有版Kubernetes。读者可根据个人情况自行选择。
    在这里插入图片描述之后,我们一路点点点,选择适合自己的配置和插件,即可完成K8S集群的创建,创建完成结果如下。
    在这里插入图片描述
    接下来要做的就是安装配置kubectl客户端,并连接到我们的K8S集群上了。之前作者在本地虚拟机部署K8S集群时,在master节点上安装过kubectl并用它对K8S集群输入命令。其实,Kubectl客户端可以在任意一台主机中部署,并连接K8S集群。

    首先我们需要从 Kubernetes 版本页面下载最新的kubectl客户端。
    在这里插入图片描述
    因为在创建集群的时候,我们设置使用公网访问,所以我们选择KubeConfig(公网访问)页签,并单击复制,将内容复制到本地计算机的 $HOME/.kube/config之中。

    配置完成后,咱们就可以使用kubectl从计算机访问Kubernetes集群了。当然,作者比较懒,直接通过自带的CloudShell使用kubectl控制的K8S。具体方法如下。
    在这里插入图片描述在这里插入图片描述点击进入即可。我们查看nodes的状态,发现我们的集群已经正常启动了。
    在这里插入图片描述(相信大家也发现了作者的K8S集群里就一个node。无奈作者家境贫寒,这也是没办法的办法。)

    至此,咱们的K8S集群的创建就算完成了。接下来,咱们就正式部署一下咱们的在线服务和离线任务。

    2. 部署在线服务

    因为阿里云已经封装好了大部分内容,这也就使得我们无需通过kubectl apply等命令去自己手动执行yaml文件了。我们只需要点击“控制台”,在新的页面点“创建”。
    在这里插入图片描述接下来,咱们只需要输入yaml文件,K8S集群将自动部署好咱们的项目。具体界面如下,这里我们选择部署一个简单的mysql作为我们的在线服务模块。如下所示,我们先后上传mysql-deployment.yaml和mysql-svc.yaml两个文件。
    在这里插入图片描述
    该两个yaml文件的完整代码可参考我之前的博客

    上传成功之后,我们可以清楚地看到CPU、内存的利用率,以及在当前命名空间中各Pod、副本集、deployment等信息。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    我们在外部能够轻松访问该Mysql,代表在线服务部署完成。

    3. 部署离线任务

    本例中,作者选择部署tf-operator、并在上边跑tensorflow程序来代表离线任务。所谓 tf-operator,就是让用户在 K8S 集群上部署训练任务更加方便和简单,大家可以参考官网和 Github 进行学习。

    在部署 tf-operator 之前,我们首先要在集群中部署Kubeflow,它是基于Kubernetes和TensorFlow的机器学习流程工具,使用Ksonnet进行应用包的管理。当然,我们必须提前部署好ksonnet。

    具体过程我就不写了,大家有兴趣可以参考这篇博客。(本例只示范用tf-operator跑一些简单的测试程序,因此Kubeflow不装也可以,但是尽量还是部署)

    我们部署好了ks和kubeflow之后,就可以正式安装tf-operator了。我们简单写个脚本并执行一下。

    # 指定工作目录
    APP_NAME=my-kubeflow
    ks init ${APP_NAME}
    cd ${APP_NAME}
    
    # 指定 ks registry,方便安装 pkg
    ks registry add kubeflow github.com/kubeflow/kubeflow/tree/master/kubeflow
    
    # 安装需要的 pkg
    ks pkg install kubeflow/common
    ks pkg install kubeflow/tf-training
    
    # all 已经可以替代所安装的 pkg 了
    ks generate all
    ks apply all
    

    理论上我们已经部署好了tf-operator,我们可以执行以下命令查看tf-operator的运行状态是否正常。

    # kubectl get pods
    

    而部署好了tf-operator之后,我们就可以跑一些tf程序了。这里我们采用官方的测试程序,一个分布式的 mnist 训练任务,原yaml文件如下。

    apiVersion: "kubeflow.org/v1"
    kind: "TFJob"
    metadata:
      name: "dist-mnist-for-e2e-test"
    spec:
      tfReplicaSpecs:
        PS:
          replicas: 2
          restartPolicy: Never
          template:
            spec:
              containers:
                - name: tensorflow
                  image: kubeflow/tf-dist-mnist-test:1.0
        Worker:
          replicas: 4
          restartPolicy: Never
          template:
            spec:
              containers:
                - name: tensorflow
                  image: kubeflow/tf-dist-mnist-test:1.0
    

    通过这个yaml文件,我们不难看出这个TFJob,作为离线计算任务,它的QoS为BestEffort。这也意味着当它和我们的在线服务之间因为资源占用问题而冲突时,它是不会影响我们的在线服务的(因为在线任务的QoS为Guaranteed)。

    我们依然可以采用直接递yaml文件的方式创建TFJob。
    在这里插入图片描述

    实际上,我们也可以通过下载源文件并执行kubectl命令行来建立该TFJob。

    # cd ./examples/v1/dist-mnist
    # docker build -f Dockerfile -t kubeflow/tf-dist-mnist-test:1.0 .
    # kubectl create -f ./tf_job_mnist.yaml
    

    可以通过以下命令来查看该Pod的状态。

    # kubectl get tfjobs.kubeflow.org dist-mnist-for-e2e-test -o yaml
    
    apiVersion: kubeflow.org/v1
    kind: TFJob
    metadata:
      creationTimestamp: "2020-03-20T10:13:14Z"
      generation: 1
      name: dist-mnist-for-e2e-test
      namespace: default
      resourceVersion: "11825"
      selfLink: /apis/kubeflow.org/v1/namespaces/default/tfjobs/dist-mnist-for-e2e-test
      uid: f3c0a2c6-b1cb-11e9-9279-0800274cd279
    spec:
      tfReplicaSpecs:
        PS:
          replicas: 1
          restartPolicy: Never
          template:
            spec:
              containers:
              - image: kubeflow/tf-dist-mnist-test:1.0
                name: tensorflow
        Worker:
          replicas: 1
          restartPolicy: Never
          template:
            spec:
              containers:
              - image: kubeflow/tf-dist-mnist-test:1.0
                name: tensorflow
    status:
      completionTime: "2020-03-20T11:21:47Z"
      conditions:
      - lastTransitionTime: "2020-03-20T11:21:47Z"
        lastUpdateTime: "2020-03-20T11:21:47Z"
        message: TFJob dist-mnist-for-e2e-test is created.
        reason: TFJobCreated
        status: "True"
        type: Created
      - lastTransitionTime: "2020-03-20T11:13:30Z"
        lastUpdateTime: "2020-03-20T11:21:47Z"
        message: TFJob dist-mnist-for-e2e-test is running.
        reason: TFJobRunning
        status: "False"
        type: Running
      - lastTransitionTime: "2020-03-20T11:21:47Z"
        lastUpdateTime: "2020-03-20T11:21:47Z"
        message: TFJob dist-mnist-for-e2e-test successfully completed.
        reason: TFJobSucceeded
        status: "True"
        type: Succeeded
      replicaStatuses:
        PS:
          succeeded: 1
        Worker:
          succeeded: 1
      startTime: "2020-03-20T11:21:47Z"
    

    四、监控模块

    在本例中,我们采用的容器监控服务依托于阿里云云监控服务,能够提供默认监控、报警规则配置等服务。只需要在创建Kubernetes集群的时候选择“云监控”插件,就可以一键解决K8S集群的监控问题。

    我们打开云监控的控制台,可以清楚地看到每一个节点的工作状态和资源利用率等指标。

    在这里插入图片描述
    点进去,可以看到单独节点的具体信息和各项指标的数据可视化。
    在这里插入图片描述阿里云监控服务的报警模块也做得十分完善。比如在某些关键服务中,我们可以根据自己业务的实际情况添加报警规则,容器监控服务会在监控指标达到告警阈值后短信通知云账号联系人。
    在这里插入图片描述阿里云的监控模块十分容易上手,在此就不赘述了。

    当我们自己使用虚拟机创建K8S集群时,为了实现对集群 的监控,我们只能自己部署监控插件,比如Prometheus。有兴趣的朋友可以移步https://www.cnblogs.com/fatyao/p/11007357.html学习Prometheus的安装和操作,作者开学之后也会尝试在自己搭建的K8S集群中采用Prometheus,在此先占个坑。

    五、小结

    作为国内云技术最优秀的互联网技术公司,阿里巴巴的云计算服务,无论从用户体验、稳定性能还是容灾管理、版本更新,都无疑是国内顶尖的。今天我们尝试了在阿里云容器服务Kubernetes版中构建一个K8S集群,并把我们的在线服务和离线任务同时部署在了K8S集群上,实现了“混合部署”的概念,并通过阿里云监控组件对系统的运行状态进行监控和预警,从而对企业生产中所应用的K8S业务部署有了实际的体验。

    展开全文
  • 文章目录Jenkins 简单自动化部署@[toc]简介说明一下下载安装Jenkins一些资源地址安装手动离线安装插件这里先说一下手动离线安装在线管理-安装更新碰到的问题创建 maven + svn + shell 的自动部署任务前提配置遇到...

    Jenkins 简单自动化部署

    Jenkins + maven + SVN + shell 测试自动部署(好像并不自动,要手动点一下)

    简介说明一下

    Jenkins只是个平台,真正运作的是它的插件,主要是啥功能的插件它都有。

    最常用来使用的目的是:作为持续集成工具。

    持续集成是一种软件开发实践,即团队开发成员经常集成他们的工作,通常每个成员至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽快地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快的开发内聚的软件。

    我感觉上就是个自动编译打包部署的东东。

    下载安装Jenkins

    一些资源地址

    安装

    下载个war的包,然后使用Tomcat,或者java -jar 启动就可以了。非常的简单。

    我用的是:

    nohup java  -Dhudson.util.ProcessTree.disable=true -jar jenkins.war --httpPort=8765  > /dev/null 2>&1 &
    

    到这里就可以访问了:http://192.168.1.212:8765/。【这里的配置在后面会说到】

    第一次登入的时候会要求配置一些相关的信息,这里如果可以联网,**一定要原则默认安装必要的插件,**不然像我一样手动安装真的很费时间。。。(ノ`Д)ノ

    这里注意下,Jenkins 的配置等信息会默认在系统/root/.jenkins/这个目录下面,包括之后新建的任务,工作空间等等。。

    手动离线安装插件

    Jenkins系统里面,最重要的就是插件了。系统本身在系统管理 》 插件管理里面就可以在线管理了(安装,更新等)。

    然而,我这次不行,提示是连不上。(这个很容易解决的)

    这里先说一下手动离线安装

    插件hpi可以去插件中心下载,这里我安装的插件有:

    • maven
    • svn
    • pubblish-oer-ssh 【这个是为了执行shell脚本不被杀点进程弄的,也不知道有没有效果】

    安装方式是在 插件管理的页面的 Advanced 标签页面下,Upload Plugin处上传插件hpi文件就可以了。

    这里需要注意的是,安装这些大的插件,本身会有很多依赖插件,所以要按依赖顺序安装。

    就是在这个地方花了很多时间,依赖会有很多,及其麻烦。。。ε=(´ο`*)))唉

    在线管理-安装更新

    我没有用这种方式安装插件,简直就是浪费时间了。

    用这种方式,就搜索需要插件,然后勾选上,点一下,去喝茶等着就可以了。

    碰到的问题

    提示:There were errors checking the update sites: SSLHandshakeException: sun.security.validator.Validator这的报错,导致无法chack 插件信息。

    解决:

    Advanced 标签页面下,Update Site选项的URL:配置为 http://updates.jenkins.io/update-center.json 即可!!!

    创建 maven + svn + shell 的自动部署任务

    前提

    系统管理 -> 全局工具配置 中先配置好JDK和Maven的路径。没有的需要先安装。

    配置

    • 创建新任务 。。。略过

    • General 配置 选择丢弃旧的构建 【据说里面的保留数量要填,我是测试环境使用就都不管了╮(╯_╰)╭】

    • Source Code Management

      • 这里选择Subversion
      • 填写URL,和Credentials用户密码
      • Repository depth :infinity
      • 勾选:Ignore externals 和 Cancel process on externals fail 和 Quiet check-out
      • Check-out Strategy : Use ‘svn update’ as much as possible
      • Repository browser : Auto
    • Build Triggers 构建模式:Build whenever a SNAPSHOT dependency is built

    • Build

      • Root POM :pom.xml
      • Goals and options : clean install
    • Post Steps 构建完成后的操作

      • 选择:Run only if build succeeds

      • command: 为执行的shell 脚本

      • rm -f /u01/csb/sys/admin-console-1.1.jar
        cp /root/.jenkins/workspace/创收宝测试版/admin-console/target/admin-console-1.1.jar /u01/csb/sys/

        cd /u01/csb/sys/

        BUILD_ID=dontKillMe ./service.sh start admin

        echo “Execute shell Finish”

    OK ,这样配置好就可以了!点个下构建,就可以喝茶等着部署好了。

    遇到问题

    问题:构建完成后调用shell 脚本里面,再调用系统中的 启动脚本 ,结果没有执行完成。

    原因:jenkins默认在build结束后会kill掉所有的衍生进程

    解决:

    • 使用java -jar启动,-Dhudson.util.ProcessTree.disable=true -jar jenkins.war
    • 在execute shell输入框中加入BUILD_ID=DONTKILLME

    我这里是上面两个解决方式都用上了。

    并且,这里的执行shell 脚本,最好是要cd到目录下./shell来执行,不然我测试的感觉都不行╮(╯_╰)╭


    完成!

    Jenkins 简单的部署,就这个样子了,其实很简单的,点点配置就可以了ε=(´ο`*)))唉

    当然,Jenkins 上还有一堆的插件,一堆的功能,以后有发现在学习了。。。。

    2019-03-23 小杭


    在这里插入图片描述

    展开全文
  • 面向对象六大原则

    万次阅读 多人点赞 2015-11-30 00:10:44
    1、优化代码的第一步——单一职责原则单一职责原则的英文名称是Single Responsibility Principle,简称SRP。它的定义是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该是一组相关性很高的...

    1、优化代码的第一步——单一职责原则

    单一职责原则的英文名称是Single Responsibility Principle,简称SRP。它的定义是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该是一组相关性很高的函数、数据的封装。就像秦小波老师在《设计模式之禅》中说的:“这是一个备受争议却又及其重要的原则。只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的”。因为单一职责的划分界限并不是总是那么清晰,很多时候都是需要靠个人经验来界定。当然,最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。
    对于计算机技术,通常只单纯地学习理论知识并不能很好地领会其深意,只有自己动手实践,并在实际运用中发现问题、解决问题、思考问题,才能够将知识吸收到自己的脑海中。下面以我的朋友小民的事迹说起。

    自从Android系统发布以来,小民就是Android的铁杆粉丝,于是在大学期间一直保持着对Android的关注,并且利用课余时间做些小项目,锻炼自己的实战能力。毕业后,小民如愿地加入了心仪的公司,并且投入到了他热爱的Android应用开发行业中。将爱好、生活、事业融为一体,小民的第一份工作也算是顺风顺水,一切尽在掌握中。
    在经历过一周的适应期以及熟悉公司的产品、开发规范之后,小民的开发工作就正式开始了。小民的主管是个工作经验丰富的技术专家,对于小民的工作并不是很满意,尤其小民最薄弱的面向对象设计,而Android开发又是使用Java语言,什么抽象、接口、六大原则、23种设计模式等名词把小民弄得晕头转向。小民自己也察觉到了自己的问题所在,于是,小民的主管决定先让小民做一个小项目来锻炼锻炼这方面的能力。正所谓养兵千日用兵一时,磨刀不误砍柴工,小民的开发之路才刚刚开始。

    在经过一番思考之后,主管挑选了使用范围广、难度也适中的ImageLoader(图片加载)作为小民的训练项目。既然要训练小民的面向对象设计,那么就必须考虑到可扩展性、灵活性,而检测这一切是否符合需求的最好途径就是开源。用户不断地提出需求、反馈问题,小民的项目需要不断升级以满足用户需求,并且要保证系统的稳定性、灵活性。在主管跟小民说了这一特殊任务之后,小民第一次感到了压力,“生活不容易呐!”年仅22岁至今未婚的小民发出了如此深刻的感叹!

    挑战总是要面对的,何况是从来不服输的小民。主管的要求很简单,要小民实现图片加载,并且要将图片缓存起来。在分析了需求之后,小民一下就放心下来了,“这么简单,原来我还以为很难呢……”小民胸有成足的喃喃自语。在经历了十分钟的编码之后,小民写下了如下代码:

    /**
     * 图片加载类
     */
    public class ImageLoader {
        // 图片缓存
        LruCache<String, Bitmap> mImageCache;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
        public ImageLoader() {
            initImageCache();
        }
    
        private void initImageCache() {
                // 计算可使用的最大内存
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
                // 取四分之一的可用内存作为缓存
            final int cacheSize = maxMemory / 4;
            mImageCache = new LruCache<String, Bitmap>(cacheSize) {
    
                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                }
            };
        }                   
    
        public  void displayImage(final String url, final ImageView imageView) {
            imageView.setTag(url);
            mExecutorService.submit(new Runnable() {
    
               @Override
                public  void run() {
                  Bitmap bitmap = downloadImage(url);
                    if (bitmap == null) {
                        return;
                    }
                    if (imageView.getTag().equals(url)) {
                        imageView.setImageBitmap(bitmap);
                    }
                    mImageCache.put(url, bitmap);
              }
           });
        }
    
        public  Bitmap downloadImage(String imageUrl) {
            Bitmap bitmap = null;
            try {
                URL url = newURL(imageUrl);
                final HttpURLConnection conn =         
                    (HttpURLConnection)url.openConnection();
                bitmap = BitmapFactory.decodeStream(
                      conn.getInputStream());
                conn.disconnect();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return bitmap;
        }
    }

    并且使用git软件进行版本控制,将工程托管到github上,伴随着git push命令的完成,小民的ImageLoader 0.1版本就正式发布了!如此短的时间内就完成了这个任务,而且还是一个开源项目,小民暗暗自喜,幻想着待会儿主管的称赞。

    在小民给主管报告了ImageLoader的发布消息的几分钟之后,主管就把小民叫到了会议室。这下小民纳闷了,怎么夸人还需要到会议室。“小民,你的ImageLoader耦合太严重啦!简直就没有设计可言,更不要说扩展性、灵活性了。所有的功能都写在一个类里怎么行呢,这样随着功能的增多,ImageLoader类会越来越大,代码也越来越复杂,图片加载系统就越来越脆弱……”Duang,这简直就是当头棒喝,小民的脑海里已经听不清主管下面说的内容了,只是觉得自己之前没有考虑清楚就匆匆忙忙完成任务,而且把任务想得太简单了。

    “你还是把ImageLoader拆分一下,把各个功能独立出来,让它们满足单一职责原则。”主管最后说道。小民是个聪明人,敏锐地捕捉到了单一职责原则这个关键词。用Google搜索了一些优秀资料之后总算是对单一职责原则有了一些认识。于是打算对ImageLoader进行一次重构。这次小民不敢过于草率,也是先画了一幅UML图,如图1-1所示。

    图1-1

    ImageLoader代码修改如下所示:

    /**
     * 图片加载类
     */
    public  class ImageLoader {
        // 图片缓存
        ImageCache mImageCache = new ImageCache() ;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
        // 加载图片
        public  void displayImage(final String url, final ImageView imageView) {
            Bitmap bitmap = mImageCache.get(url);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }
            imageView.setTag(url);
            mExecutorService.submit(new Runnable() {
    
                @Override
                public void run() {
                Bitmap bitmap = downloadImage(url);
                    if (bitmap == null) {
                        return;
                    }
                    if (imageView.getTag().equals(url)) {
                        imageView.setImageBitmap(bitmap);
                    }
                    mImageCache.put(url, bitmap);
                }
            });
         }
    
        public  Bitmap downloadImage(String imageUrl) {
            Bitmap bitmap = null;
            try {
                URL url = new URL(imageUrl);
                final HttpURLConnection conn = 
                (HttpURLConnection) 
                            url.openConnection();
                bitmap = BitmapFactory.decodeStream(conn.getInputStream());
                conn.disconnect();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return bitmap;
        }
    }   

    并且添加了一个ImageCache类用于处理图片缓存,具体代码如下:

    public class ImageCache {
        // 图片LRU缓存
        LruCache<String, Bitmap> mImageCache;
    
        public ImageCache() {
            initImageCache();
        }
    
        private void initImageCache() {
             // 计算可使用的最大内存
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
            // 取四分之一的可用内存作为缓存
            final int cacheSize = maxMemory / 4;
            mImageCache = new LruCache<String, Bitmap>(cacheSize) {
    
                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getRowBytes() *  
                        bitmap.getHeight() / 1024;
               }
            };
         }
    
        public void put(String url, Bitmap bitmap) {
            mImageCache.put(url, bitmap) ;
        }
    
        public Bitmap get(String url) {
            return mImageCache.get(url) ;
        }
    }

    如图1-1和上述代码所示,小民将ImageLoader一拆为二,ImageLoader只负责图片加载的逻辑,而ImageCache只负责处理图片缓存的逻辑,这样ImageLoader的代码量变少了,职责也清晰了,当与缓存相关的逻辑需要改变时,不需要修改ImageLoader类,而图片加载的逻辑需要修改时也不会影响到缓存处理逻辑。主管在审核了小民的第一次重构之后,对小民的工作给予了表扬,大致意思是结构变得清晰了许多,但是可扩展性还是比较欠缺,虽然没有得到主管的完全肯定,但也是颇有进步,再考虑到自己确实有所收获,小民原本沮丧的心里也略微地好转起来。

    从上述的例子中我们能够体会到,单一职责所表达出的用意就是“单一”二字。正如上文所说,如何划分一个类、一个函数的职责,每个人都有自己的看法,这需要根据个人经验、具体的业务逻辑而定。但是,它也有一些基本的指导原则,例如,两个完全不一样的功能就不应该放在一个类中。一个类中应该是一组相关性很高的函数、数据的封装。工程师可以不断地审视自己的代码,根据具体的业务、功能对类进行相应的拆分,我想这会是你优化代码迈出的第一步。

    2、让程序更稳定、更灵活——开闭原则

    开闭原则的英文全称是Open Close Principle,简称OCP,它是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。当然,在现实开发中,只通过继承的方式来升级、维护原有系统只是一个理想化的愿景,因此,在实际的开发过程中,修改原有代码、扩展代码往往是同时存在的。

    软件开发过程中,最不会变化的就是变化本身。产品需要不断地升级、维护,没有一个产品从第一版本开发完就再没有变化了,除非在下个版本诞生之前它已经被终止。而产品需要升级,修改原来的代码就可能会引发其他的问题。那么如何确保原有软件模块的正确性,以及尽量少地影响原有模块,答案就是尽量遵守本章要讲述的开闭原则。

    勃兰特·梅耶在1988年出版的《面向对象软件构造》一书中提出这一原则。这一想法认为,一旦完成,一个类的实现只应该因错误而被修改,新的或者改变的特性应该通过新建不同的类实现。新建的类可以通过继承的方式来重用原类的代码。显然,梅耶的定义提倡实现继承,已存在的实现对于修改是封闭的,但是新的实现类可以通过覆写父类的接口应对变化。
    说了这么多,想必大家还是半懂不懂,还是让我们以一个简单示例说明一下吧。

    在对ImageLoader进行了一次重构之后,小民的这个开源库获得了一些用户。小民第一次感受到自己发明“轮子”的快感,对开源的热情也越发高涨起来!通过动手实现一些开源库来深入学习相关技术,不仅能够提升自我,也能更好地将这些技术运用到工作中,从而开发出更稳定、优秀的应用,这就是小民的真实想法。

    小民第一轮重构之后的ImageLoader职责单一、结构清晰,不仅获得了主管的一点肯定,还得到了用户的夸奖,算是个不错的开始。随着用户的增多,有些问题也暴露出来了,小民的缓存系统就是大家“吐槽”最多的地方。通过内存缓存解决了每次从网络加载图片的问题,但是,Android应用的内存很有限,且具有易失性,即当应用重新启动之后,原来已经加载过的图片将会丢失,这样重启之后就需要重新下载!这又会导致加载缓慢、耗费用户流量的问题。小民考虑引入SD卡缓存,这样下载过的图片就会缓存到本地,即使重启应用也不需要重新下载了!小民在和主管讨论了该问题之后就投入了编程中,下面就是小民的代码。
    DiskCache.java类,将图片缓存到SD卡中:

    public class DiskCache {
        // 为了简单起见临时写个路径,在开发中请避免这种写法 !
        static String cacheDir = "sdcard/cache/";
         // 从缓存中获取图片
        public Bitmap get(String url) {
            return BitmapFactory.decodeFile(cacheDir + url);
        }
    
        // 将图片缓存到内存中
        public  void  put(String url, Bitmap bmp) {
           FileOutputStream fileOutputStream = null;
            try {
                fileOutputStream = new 
                     FileOutputStream(cacheDir + url);
                bmp.compress(CompressFormat.PNG, 
                     100, fileOutputStream);
          } catch (FileNotFoundException e) {
                e.printStackTrace();
          } final ly {
                if (fileOutputStream != null) {
                    try {
                        fileOutputStream.close();
                  } catch (IOException e) {
                        e.printStackTrace();
                 }
              }
          }
        }
    }

    因为需要将图片缓存到SD卡中,所以,ImageLoader代码有所更新,具体代码如下:

    public class ImageLoader {
        // 内存缓存
        ImageCache mImageCache = new ImageCache();
        // SD卡缓存
        DiskCache mDiskCache = new DiskCache();
        // 是否使用SD卡缓存
        boolean isUseDiskCache = false;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
    
        public  void displayImage(final String url, final ImageView imageView) {
            // 判断使用哪种缓存
           Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) 
                    : mImageCache.get (url);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
           }
            // 没有缓存,则提交给线程池进行下载
        }
    
        public void useDiskCache(boolean useDiskCache) {
            isUseDiskCache = useDiskCache ;
        }
    }

    从上述的代码中可以看到,仅仅新增了一个DiskCache类和往ImageLoader类中加入了少量代码就添加了SD卡缓存的功能,用户可以通过useDiskCache方法来对使用哪种缓存进行设置,例如:

    ImageLoader imageLoader = new ImageLoader() ;
     // 使用SD卡缓存
    imageLoader.useDiskCache(true);
    // 使用内存缓存
    imageLoader.useDiskCache(false);

    通过useDiskCache方法可以让用户设置不同的缓存,非常方便啊!小民对此很满意,于是提交给主管做代码审核。“小民,你思路是对的,但是有些明显的问题,就是使用内存缓存时用户就不能使用SD卡缓存,类似的,使用SD卡缓存时用户就不能使用内存缓存。用户需要这两种策略的综合,首先缓存优先使用内存缓存,如果内存缓存没有图片再使用SD卡缓存,如果SD卡中也没有图片最后才从网络上获取,这才是最好的缓存策略。”主管真是一针见血,小民这时才如梦初醒,刚才还得意洋洋的脸上突然有些泛红……
    于是小民按照主管的指点新建了一个双缓存类DoudleCache,具体代码如下:

    /**
     * 双缓存。获取图片时先从内存缓存中获取,如果内存中没有缓存该图片,再从SD卡中获取。
     *  缓存图片也是在内存和SD卡中都缓存一份
     */
    public class DoubleCache {
        ImageCache mMemoryCache = new ImageCache();
        DiskCache mDiskCache = new DiskCache();
    
        // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
        public   Bitmap get(String url) {
           Bitmap bitmap = mMemoryCache.get(url);
            if (bitmap == null) {
                bitmap = mDiskCache.get(url);
            }
            return  bitmap;
        }
    
        // 将图片缓存到内存和SD卡中
        public void put(String url, Bitmap bmp) {
            mMemoryCache.put(url, bmp);
            mDiskCache.put(url, bmp);
       }
    }

    我们再看看最新的ImageLoader类吧,代码更新也不多:

    public class ImageLoader {
        // 内存缓存
        ImageCache mImageCache = new ImageCache();
        // SD卡缓存
        DiskCache mDiskCache = new DiskCache();
        // 双缓存
        DoubleCache mDoubleCache = new DoubleCache() ;
        // 使用SD卡缓存
        boolean isUseDiskCache = false;
        // 使用双缓存
        boolean isUseDoubleCache = false;
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
    
        public void displayImage(final String url, final ImageView imageView) {
            Bitmap bmp = null;
             if (isUseDoubleCache) {
                bmp = mDoubleCache.get(url);
            } else if (isUseDiskCache) {
                bmp = mDiskCache.get(url);
            } else {
                bmp = mImageCache.get(url);
            }
    
             if ( bmp != null ) {
                imageView.setImageBitmap(bmp);
            }
            // 没有缓存,则提交给线程池进行异步下载图片
        }
    
        public void useDiskCache(boolean useDiskCache) {
            isUseDiskCache = useDiskCache ;
        }
    
        public void useDoubleCache(boolean useDoubleCache) {
            isUseDoubleCache = useDoubleCache ;
        }
    }

    通过增加短短几句代码和几处修改就完成了如此重要的功能。小民已越发觉得自己Android开发已经到了的得心应手的境地,不仅感觉一阵春风袭来,他那飘逸的头发一下从他的眼前拂过,小民感觉今天天空比往常敞亮许多。

    “小民,你每次加新的缓存方法时都要修改原来的代码,这样很可能会引入Bug,而且会使原来的代码逻辑变得越来越复杂,按照你这样的方法实现,用户也不能自定义缓存实现呀!”到底是主管水平高,一语道出了小民这缓存设计上的问题。

    我们还是来分析一下小民的程序,小民每次在程序中加入新的缓存实现时都需要修改ImageLoader类,然后通过一个布尔变量来让用户使用哪种缓存,因此,就使得在ImageLoader中存在各种if-else判断,通过这些判断来确定使用哪种缓存。随着这些逻辑的引入,代码变得越来越复杂、脆弱,如果小民一不小心写错了某个if条件(条件太多,这是很容易出现的),那就需要更多的时间来排除。整个ImageLoader类也会变得越来越臃肿。最重要的是用户不能自己实现缓存注入到ImageLoader中,可扩展性可是框架的最重要特性之一。

    “软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的,这就是开放-关闭原则。也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。”小民的主管补充到,小民听得云里雾里的。主管看小民这等反应,于是亲自“操刀”,为他画下了如图1-2的UML图。


    图1-2

    小民看到图1-2似乎明白些什么,但是又不是太明确如何修改程序。主管看到小民这般模样只好亲自上阵,带着小民把ImageLoader程序按照图1-2进行了一次重构。具体代码如下:

    public class ImageLoader {
        // 图片缓存
        ImageCache mImageCache = new MemoryCache();
        // 线程池,线程数量为CPU的数量
        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
    
        // 注入缓存实现
        public void setImageCache(ImageCache cache) {
            mImageCache = cache;
        }
    
        public void displayImage(String imageUrl, ImageView imageView) {
            Bitmap bitmap = mImageCache.get(imageUrl);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }
            // 图片没缓存,提交到线程池中下载图片
            submitLoadRequest(imageUrl, imageView);
        }
    
        private void submitLoadRequest(final String imageUrl,
                 final ImageView imageView) {
            imageView.setTag(imageUrl);
            mExecutorService.submit(new Runnable() {
    
                @Override
                public  void run() {
                  Bitmap bitmap = downloadImage(imageUrl);
                    if (bitmap == null) {
                        return;
                 }
                   if (imageView.getTag().equals(imageUrl)) {
                        imageView.setImageBitmap(bitmap);
                 }
                    mImageCache.put(imageUrl, bitmap);
             }
          });
        }
    
        public  Bitmap downloadImage(String imageUrl) {
           Bitmap bitmap = null;
            try {
               URL url = new URL(imageUrl);
                final HttpURLConnection conn = (HttpURLConnection) 
                            url.openConnection();
                bitmap = BitmapFactory.decodeStream(conn.getInputStream());
                conn.disconnect();
            } catch (Exception e) {
                  e.printStackTrace();
            }
    
            return bitmap;
        }
    }

    经过这次重构,没有了那么多的if-else语句,没有了各种各样的缓存实现对象、布尔变量,代码确实清晰、简单了很多,小民对主管的崇敬之情又“泛滥”了起来。需要注意的是,这里的ImageCache类并不是小民原来的那个ImageCache,这次程序重构主管把它提取成一个图片缓存的接口,用来抽象图片缓存的功能。我们看看该接口的声明:

    public interface ImageCache {
        public Bitmap get(String url);
        public void put(String url, Bitmap bmp);
    }

    ImageCache接口简单定义了获取、缓存图片两个函数,缓存的key是图片的url,值是图片本身。内存缓存、SD卡缓存、双缓存都实现了该接口,我们看看这几个缓存实现:

    // 内存缓存MemoryCache类
    public class MemoryCache implements ImageCache {
        private LruCache<String, Bitmap> mMemeryCache;
    
        public MemoryCache() {
            // 初始化LRU缓存
        }
    
         @Override
        public Bitmap get(String url) {
            return mMemeryCache.get(url);
        }
    
        @Override
        public void put(String url, Bitmap bmp) {
            mMemeryCache.put(url, bmp);
        }
    }
    
    // SD卡缓存DiskCache类
    public  class  DiskCache implements ImageCache {
        @Override
        public Bitmap get(String url) {
            return null/* 从本地文件中获取该图片 */;
        }
    
        @Override
        public void put(String url, Bitmap bmp) {
            // 将Bitmap写入文件中
        }
    }
    
    // 双缓存DoubleCache类
    public class DoubleCache implements ImageCache{
        ImageCache mMemoryCache = new MemoryCache();
        ImageCache mDiskCache = new DiskCache();
    
        // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
        public Bitmap get(String url) {
           Bitmap bitmap = mMemoryCache.get(url);
            if (bitmap == null) {
                bitmap = mDiskCache.get(url);
           }
            return bitmap;
         }
    
        // 将图片缓存到内存和SD卡中
        public void put(String url, Bitmap bmp) {
            mMemoryCache.put(url, bmp);
            mDiskCache.put(url, bmp);
        }
    }

    细心的朋友可能注意到了,ImageLoader类中增加了一个setImageCache(ImageCache cache)函数,用户可以通过该函数设置缓存实现,也就是通常说的依赖注入。下面就看看用户是如何设置缓存实现的:

    ImageLoader imageLoader = new ImageLoader() ;
            // 使用内存缓存
    imageLoader.setImageCache(new MemoryCache());
            // 使用SD卡缓存
    imageLoader.setImageCache(new DiskCache());
            // 使用双缓存
    imageLoader.setImageCache(new DoubleCache());
            // 使用自定义的图片缓存实现
    imageLoader.setImageCache(new ImageCache() {
    
                @Override
            public void put(String url, Bitmap bmp) {
                // 缓存图片
           }
    
                @Override
            public Bitmap get(String url) {
                return null/*从缓存中获取图片*/;
           }
        });

    在上述代码中,通过setImageCache(ImageCache cache)方法注入不同的缓存实现,这样不仅能够使ImageLoader更简单、健壮,也使得ImageLoader的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache缓存图片的具体实现完全不一样,但是,它们的一个特点是都实现了ImageCache接口。当用户需要自定义实现缓存策略时,只需要新建一个实现ImageCache接口的类,然后构造该类的对象,并且通过setImageCache(ImageCache cache)注入到ImageLoader中,这样ImageLoader就实现了变化万千的缓存策略,而扩展这些缓存策略并不会导致ImageLoader类的修改。经过这次重构,小民的ImageLoader已经基本算合格了。咦!这不就是主管说的开闭原则么!“软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。而遵循开闭原则的重要手段应该是通过抽象……”小民细声细语的念叨中,陷入了思索中……

    开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。这里的“应该尽量”4个字说明OCP原则并不是说绝对不可以修改原始类的,当我们嗅到原来的代码“腐化气味”时,应该尽早地重构,以使得代码恢复到正常的“进化”轨道,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。我们的开发过程中也没有那么理想化的状况,完全地不用修改原来的代码,因此,在开发过程中需要自己结合具体情况进行考量,是通过修改旧代码还是通过继承使得软件系统更稳定、更灵活,在保证去除“代码腐化”的同时,也保证原有模块的正确性。

    3、构建扩展性更好的系统——里氏替换原则

    里氏替换原则英文全称是Liskov Substitution Principle,简称LSP。它的第一种定义是:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。上面这种描述确实不太好理解,理论家有时候容易把问题抽象化,本来挺容易理解的事让他们一概括就弄得拗口了。我们再看看另一个直截了当的定义。里氏替换原则第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。

    我们知道,面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是,所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。说了那么多,其实最终总结就两个字:抽象。
    小民为了深入地了解Android中的Window与View的关系特意写了一个简单示例,为了便于理解,我们先看如图1-3所示。

    ▲图1-3

    我们看看具体的代码:

    // 窗口类
    public class Window {
        public void show(View child){
            child.draw();
        }
    }
    
    // 建立视图抽象,测量视图的宽高为公用代码,绘制交给具体的子类
    public abstract class  View {
        public abstract void  draw() ;
        public void  measure(int width, int height){
            // 测量视图大小
        }
    }
    
    // 按钮类的具体实现
    public class Button extends View {
        public void draw(){
            // 绘制按钮
        }
    }
    // TextView的具体实现
    public class TextView extends View {
        public void draw(){
            // 绘制文本
        }
    }

    上述示例中,Window依赖于View,而View定义了一个视图抽象,measure是各个子类共享的方法,子类通过覆写View的draw方法实现具有各自特色的功能,在这里,这个功能就是绘制自身的内容。任何继承自View类的子类都可以设置给show方法,也就我们所说的里氏替换。通过里氏替换,就可以自定义各式各样、千变万化的View,然后传递给Window,Window负责组织View,并且将View显示到屏幕上。
    里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性,在OOP当中,继承的优缺点都相当明显。
    优点如下:

    • (1)代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性;
    • (2)子类与父类基本相似,但又与父类有所区别;
    • (3)提高代码的可扩展性。

    继承的缺点:

    • (1)继承是侵入性的,只要继承就必须拥有父类的所有属性和方法;
    • (2)可能造成子类代码冗余、灵活性降低,因为子类必须拥有父类的属性和方法。

    事物总是具有两面性,如何权衡利与弊都是需要根据具体场景来做出选择并加以处理。里氏替换原则指导我们构建扩展性更好的软件系统,我们还是接着上面的ImageLoader来做说明。
    上文的图1-2也很好地反应了里氏替换原则,即MemoryCache、DiskCache、DoubleCache都可以替换ImageCache的工作,并且能够保证行为的正确性。ImageCache建立了获取缓存图片、保存缓存图片的接口规范,MemoryCache等根据接口规范实现了相应的功能,用户只需要在使用时指定具体的缓存对象就可以动态地替换ImageLoader中的缓存策略。这就使得ImageLoader的缓存系统具有了无线的可能性,也就是保证了可扩展性。

    想象一个场景,当ImageLoader中的setImageCache(ImageCache cache)中的cache对象不能够被子类所替换,那么用户如何设置不同的缓存对象以及用户如何自定义自己的缓存实现,通过1.3节中的useDiskCache方法吗?显然不是的,里氏替换原则就为这类问题提供了指导原则,也就是建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的高扩展性、灵活性。开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换来达到对扩展开放,对修改关闭的效果。然而,这两个原则都同时强调了一个OOP的重要特性——抽象,因此,在开发过程中运用抽象是走向代码优化的重要一步。

    4、 让项目拥有变化的能力——依赖倒置原则

    依赖倒置原则英文全称是Dependence Inversion Principle,简称DIP。依赖反转原则指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。这个概念有点不好理解,这到底是什么意思呢?
    依赖倒置原则的几个关键点:

    • (1)高层模块不应该依赖低层模块,两者都应该依赖其抽象;
    • (2)抽象不应该依赖细节;
    • (3)细节应该依赖抽象。

    在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是,可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。高层模块就是调用端,低层模块就是具体实现类。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。这又是一个将理论抽象化的实例,其实一句话就可以概括:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。面向接口编程是面向对象精髓之一,也就是上面两节强调的抽象。

    如果在类与类直接依赖于细节,那么它们之间就有直接的耦合,当具体实现需要变化时,意味着在这要同时修改依赖者的代码,并且限制了系统的可扩展性。我们看1.3节的图1-3中,ImageLoader直接依赖于MemoryCache,这个MemoryCache是一个具体实现,而不是一个抽象类或者接口。这导致了ImageLoader直接依赖了具体细节,当MemoryCache不能满足ImageLoader而需要被其他缓存实现替换时,此时就必须修改ImageLoader的代码,例如:

    public class ImageLoader {
        // 内存缓存 ( 直接依赖于细节 )
        MemoryCache mMemoryCache = new MemoryCache();
         // 加载图片到ImageView中
        public void displayImage(String url, ImageView imageView) {
           Bitmap bmp = mMemoryCache.get(url);
            if (bmp == null) {
                downloadImage(url, imageView);
            } else {
                imageView.setImageBitmap(bmp);
            }
        }
    
        public void setImageCache(MemoryCache cache) {
            mCache = cache ;
        }
        // 代码省略
    }

    随着产品的升级,用户发现MemoryCache已经不能满足需求,用户需要小民的ImageLoader可以将图片同时缓存到内存和SD卡中,或者可以让用户自定义实现缓存。此时,我们的MemoryCache这个类名不仅不能够表达内存缓存和SD卡缓存的意义,也不能够满足功能。另外,用户需要自定义缓存实现时还必须继承自MemoryCache,而用户的缓存实现可不一定与内存缓存有关,这在命名上的限制也让用户体验不好。重构的时候到了!小民的第一种方案是将MemoryCache修改为DoubleCache,然后在DoubleCache中实现具体的缓存功能。我们需要将ImageLoader修改如下:

    public class ImageLoader {
        // 双缓存 ( 直接依赖于细节 )
        DoubleCache mCache = new DoubleCache();
        // 加载图片到ImageView中
        public void displayImage(String url, ImageView imageView) {
           Bitmap bmp = mCache.get(url);
            if (bmp == null) {
              // 异步下载图片
                downloadImageAsync(url, imageView);
           } else {
                imageView.setImageBitmap(bmp);
           }
        }
    
        public void setImageCache(DoubleCache cache) {
             mCache = cache ;
        }
        // 代码省略
    }

    我们将MemoryCache修改成DoubleCache,然后修改了ImageLoader中缓存类的具体实现,轻轻松松就满足了用户需求。等等!这不还是依赖于具体的实现类(DoubleCache)吗?当用户的需求再次变化时,我们又要通过修改缓存实现类和ImageLoader代码来实现?修改原有代码不是违反了1.3节中的开闭原则吗?小民突然醒悟了过来,低下头思索着如何才能让缓存系统更灵活、拥抱变化……

    当然,这些都是在主管给出图1-2(1.3节)以及相应的代码之前,小民体验的煎熬过程。既然是这样,那显然主管给出的解决方案就能够让缓存系统更加灵活。一句话概括起来就是:依赖抽象,而不依赖具体实现。针对于图片缓存,主管建立的ImageCache抽象,该抽象中增加了get和put方法用以实现图片的存取。每种缓存实现都必须实现这个接口,并且实现自己的存取方法。当用户需要使用不同的缓存实现时,直接通过依赖注入即可,保证了系统的灵活性。我们再来简单回顾一下相关代码:

    ImageCache缓存抽象:

    public interface ImageCache {
        public Bitmap get(String url);
        public void put(String url, Bitmap bmp);
    }

    ImageLoader类:

    public class ImageLoader {
        // 图片缓存类,依赖于抽象,并且有一个默认的实现
        ImageCache mCache = new MemoryCache();
    
        // 加载图片
        public void displayImage(String url, ImageView imageView) {
           Bitmap bmp = mCache.get(url);
            if (bmp == null) {
            // 异步加载图片
                downloadImageAsync(url, imageView);
           } else {
                imageView.setImageBitmap(bmp);
           }
        }
    
        /**
         * 设置缓存策略,依赖于抽象
         */
        public void setImageCache(ImageCache cache) {
            mCache = cache;
        }
        // 代码省略
    }

    在这里,我们建立了ImageCache抽象,并且让ImageLoader依赖于抽象而不是具体细节。当需求发生变更时,小民只需要实现ImageCahce类或者继承其他已有的ImageCache子类完成相应的缓存功能,然后将具体的实现注入到ImageLoader即可实现缓存功能的替换,这就保证了缓存系统的高可扩展性,拥有了拥抱变化的能力,而这一切的基本指导原则就是我们的依赖倒置原则。从上述几节中我们发现,要想让我们的系统更为灵活,抽象似乎成了我们唯一的手段。

    5、系统有更高的灵活性——接口隔离原则

    接口隔离原则英文全称是InterfaceSegregation Principles,简称ISP。它的定义是:客户端不应该依赖它不需要的接口。另一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。

    接口隔离原则说白了就是,让客户端依赖的接口尽可能地小,这样说可能还是有点抽象,我们还是以一个示例来说明一下。在此之前我们来说一个场景,在Java 6以及之前的JDK版本,有一个非常讨厌的问题,那就是在使用了OutputStream或者其他可关闭的对象之后,我们必须保证它们最终被关闭了,我们的SD卡缓存类中就有这样的代码:

    // 将图片缓存到内存中
    public void put(String url, Bitmap bmp) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
              } catch (IOException e) {
                    e.printStackTrace();
              }
           } // end if
        } // end if finally
    }

    我们看到的这段代码可读性非常差,各种try…catch嵌套,都是些简单的代码,但是会严重影响代码的可读性,并且多层级的大括号很容易将代码写到错误的层级中。大家应该对这类代码也非常反感,那我们看看如何解决这类问题。
    我们可能知道Java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法,如图1-4所示。
    我们要讲的FileOutputStream类就实现了这个接口,我们从图1-4中可以看到,还有一百多个类实现了Closeable这个接口,这意味着,在关闭这一百多个类型的对象时,都需要写出像put方法中finally代码段那样的代码。这还了得!你能忍,反正小民是忍不了的!于是小民打算要发挥他的聪明才智解决这个问题,既然都是实现了Closeable接口,那只要我建一个方法统一来关闭这些对象不就可以了么?说干就干,于是小民写下来如下的工具类:

    ▲图1-4

    public final class CloseUtils {
    
        Private CloseUtils() { }
    
        /**
         * 关闭Closeable对象
         * @param closeable
         */
        public static void closeQuietly(Closeable closeable) {
            if (null != closeable) {
                try {
                    closeable.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
           }
        }
    }

    我们再看看把这段代码运用到上述的put方法中的效果如何:

    public void put(String url, Bitmap bmp) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
       } catch (FileNotFoundException e) {
            e.printStackTrace();
       } final ly {
            CloseUtils.closeQuietly(fileOutputStream);
       }
    }

    代码简洁了很多!而且这个closeQuietly方法可以运用到各类可关闭的对象中,保证了代码的重用性。CloseUtils的closeQuietly方法的基本原理就是依赖于Closeable抽象而不是具体实现(这不是1.4节中的依赖倒置原则么),并且建立在最小化依赖原则的基础,它只需要知道这个对象是可关闭,其他的一概不关心,也就是这里的接口隔离原则。

    试想一下,如果在只是需要关闭一个对象时,它却暴露出了其他的接口函数,比如OutputStream的write方法,这就使得更多的细节暴露在客户端代码面前,不仅没有很好地隐藏实现,还增加了接口的使用难度。而通过Closeable接口将可关闭的对象抽象起来,这样只需要客户端依赖于Closeable就可以对客户端隐藏其他的接口信息,客户端代码只需要知道这个对象可关闭(只可调用close方法)即可。小民ImageLoader中的ImageCache就是接口隔离原则的运用,ImageLoader只需要知道该缓存对象有存、取缓存图片的接口即可,其他的一概不管,这就使得缓存功能的具体实现对ImageLoader具体的隐藏。这就是用最小化接口隔离了实现类的细节,也促使我们将庞大的接口拆分到更细粒度的接口当中,这使得我们的系统具有更低的耦合性,更高的灵活性。

    Bob大叔(Robert C Martin)在21世纪早期将单一职责、开闭原则、里氏替换、接口隔离以及依赖倒置(也称为依赖反转)5个原则定义为SOLID原则,指代了面向对象编程的5个基本原则。当这些原则被一起应用时,它们使得一个软件系统更清晰、简单、最大程度地拥抱变化。SOLID被典型地应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发基本原则的重要组成部分。在经过第1.1~1.5节的学习之后,我们发现这几大原则最终就可以化为这几个关键词:抽象、单一职责、最小化。那么在实际开发过程中如何权衡、实践这些原则,是大家需要在实践中多思考与领悟,正所谓”学而不思则罔,思而不学则殆”,只有不断地学习、实践、思考,才能够在积累的过程有一个质的飞越。

    6、更好的可扩展性——迪米特原则

    迪米特原则英文全称为Law of Demeter,简称LOD,也称为最少知识原则(Least Knowledge Principle)。虽然名字不同,但描述的是同一个原则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现、如何复杂都与调用者或者依赖者没关系,调用者或者依赖者只需要知道他需要的方法即可,其他的我一概不关心。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

    迪米特法则还有一个英文解释是:Only talk to your immedate friends,翻译过来就是:只与直接的朋友通信。什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。

    光说不练很抽象呐,下面我们就以租房为例来讲讲迪米特原则。
    “北漂”的同学比较了解,在北京租房绝大多数都是通过中介找房。我们设定的情境为:我只要求房间的面积和租金,其他的一概不管,中介将符合我要求的房子提供给我就可以。下面我们看看这个示例:

    /**
     * 房间
     */
    public class Room {
        public float area;
        public float price;
    
        public Room(float  area, float  price) {
            this.area = area;
            this.price = price;
        }
    
        @Override
        public String toString() {
            return "Room [area=" + area + ", price=" + price + "]";
        }
    
    }
    
    /**
     * 中介
     */
    public class Mediator {
        List<Room> mRooms = new ArrayList<Room>();
    
        public Mediator() {
            for (inti = 0; i < 5; i++) {
                mRooms.add(new Room(14 + i, (14 + i) * 150));
           }
       }
    
        public List<Room>getAllRooms() {
            return mRooms;
       }
    }
    
    
    /**
     * 租户
     */
    public class Tenant {
        public float roomArea;
        public float roomPrice;
        public static final float diffPrice = 100.0001f;
        public static final float diffArea = 0.00001f;
    
        public void rentRoom(Mediator mediator) {
            List<Room>rooms = mediator.getAllRooms();
            for (Room room : rooms) {
                if (isSuitable(room)) {
                 System.out.println("租到房间啦! " + room);
                    break;
              }
           }
        }
    
        private boolean isSuitable(Room room) {
            return Math.abs(room.price - roomPrice) < diffPrice
                    &&Math.abs(room.area - roomArea) < diffArea;
       }
    }

    从上面的代码中可以看到,Tenant不仅依赖了Mediator类,还需要频繁地与Room类打交道。租户类的要求只是通过中介找到一间适合自己的房间罢了,如果把这些检测条件都放在Tenant类中,那么中介类的功能就被弱化,而且导致Tenant与Room的耦合较高,因为Tenant必须知道许多关于Room的细节。当Room变化时Tenant也必须跟着变化。Tenant又与Mediator耦合,就导致了纠缠不清的关系。这个时候就需要我们分清谁才是我们真正的“朋友”,在我们所设定的情况下,显然是Mediator(虽然现实生活中不是这样的)。上述代码的结构如图1-5所示。

    ▲图1-5

    既然是耦合太严重,那我们就只能解耦了,首先要明确地是,我们只和我们的朋友通信,这里就是指Mediator对象。必须将Room相关的操作从Tenant中移除,而这些操作案例应该属于Mediator,我们进行如下重构:

    /**
     * 中介
     */
    public class Mediator {
        List<Room> mRooms = new ArrayList<Room>();
    
        public Mediator() {
            for (inti = 0; i < 5; i++) {
                mRooms.add(new Room(14 + i, (14 + i) * 150));
           }
        }
    
        public Room rentOut(float  area, float  price) {
            for (Room room : mRooms) {
                if (isSuitable(area, price, room)) {
                    return  room;
              }
           }
            return null;
        }
    
        private boolean isSuitable(float area, float price, Room room) {
            return Math.abs(room.price - price) < Tenant.diffPrice
                && Math.abs(room.area - area) < Tenant.diffPrice;
        }
    }
    
    /**
     * 租户
     */
    public class Tenant {
    
        public float roomArea;
        public float roomPrice;
        public static final float diffPrice = 100.0001f;
        public static final float diffArea = 0.00001f;
    
        public void rentRoom(Mediator mediator) {
            System.out.println("租到房啦 " + mediator.rentOut(roomArea, roomPrice));
         }
    }

    重构后的结构图如图1-6所示。

    ▲图1-6

    只是将对于Room的判定操作移到了Mediator类中,这本应该是Mediator的职责,他们根据租户设定的条件查找符合要求的房子,并且将结果交给租户就可以了。租户并不需要知道太多关于Room的细节,比如与房东签合同、房东的房产证是不是真的、房内的设施坏了之后我要找谁维修等,当我们通过我们的“朋友”中介租了房之后,所有的事情我们都通过与中介沟通就好了,房东、维修师傅等这些角色并不是我们直接的“朋友”。“只与直接的朋友通信”这简单的几个字就能够将我们从乱七八糟的关系网中抽离出来,使我们的耦合度更低、稳定性更好。
    通过上述示例以及小民的后续思考,迪米特原则这把利剑在小民的手中已经舞得风生水起。就拿sd卡缓存来说吧,ImageCache就是用户的直接朋友,而SD卡缓存内部却是使用了jake wharton的DiskLruCache实现,这个DiskLruCache就不属于用户的直接朋友了,因此,用户完全不需要知道它的存在,用户只需要与ImageCache对象打交道即可。例如将图片存到SD卡中的代码如下。

    public void put(String url, Bitmap value) {
        DiskLruCache.Editor editor = null;
        try {
           // 如果没有找到对应的缓存,则准备从网络上请求数据,并写入缓存
            editor = mDiskLruCache.edit(url);
            if (editor != null) {
                    OutputStream outputStream = editor.newOutputStream(0);
                if (writeBitmapToDisk(value, outputStream)) {
                  // 写入disk缓存
                    editor.commit();
              } else {
                    editor.abort();
              }
                CloseUtils.closeQuietly(outputStream);
           }
        } catch (IOException e) {
             e.printStackTrace();
        }
    }

    用户在使用SD卡缓存时,根本不知晓DiskLruCache的实现,这就很好地对用户隐藏了具体实现。当小民已经“牛”到可以自己完成SD卡的rul实现时,他就可以随心所欲的替换掉jake wharton的DiskLruCache。小民的代码大体如下:

    @Override
    public  void put(String url, Bitmap bmp) {
        // 将Bitmap写入文件中
        FileOutputStream fos = null;
        try {
           // 构建图片的存储路径 ( 省略了对url取md5)
            fos = new FileOutputStream("sdcard/cache/" + imageUrl2MD5(url));
            bmp.compress(CompressFormat.JPEG, 100, fos);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if ( fos != null ) {
                try {
                    fos.close();
              } catch (IOException e) {
                    e.printStackTrace();
              }
           }
        } // end if finally
    }

    SD卡缓存的具体实现虽然被替换了,但用户根本不会感知到。因为用户根本不知道DiskLruCache的存在,他们没有与DiskLruCache进行通信,他们只认识直接“朋友”ImageCache,ImageCache将一切细节隐藏在了直接“朋友”的外衣之下,使得系统具有更低的耦合性和更好的可扩展性。

    7、总结

    在应用开发过程中,最难的不是完成应用的开发工作,而是在后续的升级、维护过程中让应用系统能够拥抱变化。拥抱变化也就意味着在满足需求且不破坏系统稳定性的前提下保持高可扩展性、高内聚、低耦合,在经历了各版本的变更之后依然保持清晰、灵活、稳定的系统架构。当然,这是一个比较理想的情况,但我们必须要朝着这个方向去努力,那么遵循面向对象六大原则就是我们走向灵活软件之路所迈出的第一步。

    展开全文
  • Kubeflow 部署采坑记录

    千次阅读 2019-08-06 19:19:29
    文章目录1 Overview2 Deploy2.1 本地部署2.2 init 过程2.2 generate 过程2.3 apply 过程2.4 部署成功2.5 删除3 必须要注意的问题4 部署失败的原因附录 Kubeflow = Kubernetes + Machine Learing + Flow 1 Overview...

    Kubeflow = Kubernetes + Machine Learing + Flow

    1 Overview

    Kubeflow 是在 K8S 集群上跑机器学习任务的工具集,提供了 Tensorflow, Pytorch 等等机器/深度学习的计算框架,同时构建容器工作流 Argo 的集成,称为 Pipeline。关于其部署,最新版本的本地部署有很多问题,Github 上的 issue 大多数都是与部署有关的,所以如果不是在 GCP 上部署,会可能碰到各种各样的问题。

    之所以对 GCP 支持这么好,是因为 Kubeflow 是 Google 内部机器学习工作流的开源版本,但是投入的核心开发者不多,版本更新和问题修复只有几个人在做。

    部署之前,请先了解几个关于 ksonnet 的概念。

    1. registry: ksonnet 的模板仓库,可以是离线也可以是线上,只要能访问即可
    2. env: registry 注册在 env 下,通过 env 切换部署模板的仓库
    3. pkg: registry 里放着的是模板,里面包含了 protoypes 和 library
    4. prototype: 本文统一叫组件,可以配置不同的 param
    5. library: 包含了 k8s 的 Api 信息,不同版本的 k8s 的 Api 不同
    6. param: 用于填充模板的参数
    7. componet: 填充了参数的模板,本文统一叫组件

    2 Deploy

    Kubeflow 的官方文档提供了各种平台的部署方案。

    https://www.kubeflow.org/docs/started/

    image_1dhj1pnekf8tc431eop13k4121g9.png-236kB

    部署方面,Kubeflow 利用了 Ksonnet,他是一个方便管理 K8S yaml 的工具。

    https://ksonnet.io/

    image_1dhj2fqedma71jeieppg8ufpfm.png-354.6kB

    因为光用 Kubeflow 提供的部署脚本,遇到问题,还是得用 ks 命令看看的,所以有必要熟悉一下(后面会结合例子稍微讲下)。

    2.1 本地部署

    首先需要搞清楚,利用脚本一键部署的组件有哪些,需要花点时间去了解各个组件,否则排错的时候,根本无从下手。

    # ks component list
    COMPONENT           TYPE    我补充的信息,未必准确
    =========           ====    ========== ==== ====
    ambassador          jsonnet Kubeflow 的认证统一网关和路由
    application         jsonnet 组件太多了,这个是做集成的 CRD
    argo                jsonnet 容器任务调度
    centraldashboard    jsonnet Kubeflow 的入口 UI
    jupyter             jsonnet jupyter
    jupyter-web-app     jsonnet jupyter hub
    katib               jsonnet 用于深度学习调参的组件
    metacontroller      jsonnet 也是一个内部的 CRD
    notebook-controller jsonnet hub 里可以增加多个 notebook,也是一个 CRD
    notebooks           jsonnet jupyter nootbook
    openvino            jsonnet 
    pipeline            jsonnet pipeline 集成 
    profiles            jsonnet 用户权限和认证方面的组件
    pytorch-operator    jsonnet 一个深度学习的框架
    spartakus           jsonnet
    tensorboard         jsonnet 
    tf-job-operator     jsonnet tensorflow 任务的 CRD
    

    这么多组件部署起来是非常麻烦的,官方给力一个脚本,但是光有脚本不够用的,出问题,还是得看看脚本内容,简单说一下结构。

    image_1dhj2slk2157r1mce1fjd8ouvq72g.png-125.3kB

    必须确保版本下载正确,否则排错又是一推问题…

    image_1dhj2vh5d1m7q7mucom1vvhd082t.png-156.7kB

    下载完就有三个文件夹。重点看看脚本文件夹。部署关键在两个脚本,kfctl.sh/util.sh。

    image_1dhj33rh61auu1fvefqqhq10bn3a.png-127kB

    因为脚本实在太长了,而且 gcp/aws/minkube 所有平台都混合一起的,重点还是要看关于 ks 的部分,因为部署的核心是用到的是 kssonet。

    image_1dhj3fe191o48bt7oc89cc117t56.png-118.5kB

    关于 ksonset,上图是非常经典的,模板是一个缺了一些部分的圆柱,比如 yaml 中的 image,meta.name 之类的参数,第二部分是这几个参数的抽象化,通过 kssonet 就把最终的圆柱补齐了,最后结合成一个完成的 yaml 文件,可以用 kubectl apply -f xxx.yaml,或者用 kssonet 的命令 ks apply -c <组件>

    通过 grep 把关键的 ks 相关的命令抓出来。

    # 运行这个命令,找到 ks 相关的命令 cat util.sh| grep "ks"
    # All deployments should call this function to create a common ksonnet app.
    # Create the ksonnet app
    # 初始化 ks 项目的目录,注意 ${KS_INIT_EXTRA_ARGS} 后面还会提到
    eval ks init $(basename "${KUBEFLOW_KS_DIR}") --skip-default-registries ${KS_INIT_EXTRA_ARGS}
    # 也是非常重要的,先删除默认的环境,这里 default 代理了 ks 的 registry
    ks env rm default
    # 注册 registry,后面的部分会跟前面几条一起描述
    ks registry add kubeflow "${KUBEFLOW_REPO}/kubeflow"
    # 从这里开始是安装各种 ks 组件模板,还没多大用处,必须 generate
    ks pkg install kubeflow/argo
    ks pkg install kubeflow/pipeline
    ks pkg install kubeflow/common
    ks pkg install kubeflow/examples
    ks pkg install kubeflow/jupyter
    ks pkg install kubeflow/katib
    ks pkg install kubeflow/mpi-job
    ks pkg install kubeflow/pytorch-job
    ks pkg install kubeflow/seldon
    ks pkg install kubeflow/tf-serving
    ks pkg install kubeflow/openvino
    ks pkg install kubeflow/tensorboard
    ks pkg install kubeflow/tf-training
    ks pkg install kubeflow/metacontroller
    ks pkg install kubeflow/profiles
    ks pkg install kubeflow/application
    ks pkg install kubeflow/modeldb
    # generate 命令是用来把参数填充到模板里的,构成前面说的完成的 yaml 
    # 注意这些组件不能一一对应前面的模板的,因为有些组件里包含了几个模板的参数
    ks generate pytorch-operator pytorch-operator
    ks generate ambassador ambassador
    ks generate openvino openvino
    ks generate jupyter jupyter
    ks generate notebook-controller notebook-controller
    ks generate jupyter-web-app jupyter-web-app
    ks generate centraldashboard centraldashboard
    ks generate tf-job-operator tf-job-operator
    ks generate tensorboard tensorboard
    ks generate metacontroller metacontroller
    ks generate profiles profiles
    ks generate notebooks notebooks
    ks generate argo argo
    ks generate pipeline pipeline
    ks generate katib katib
    # cd ks_app
    # ks component rm spartakus
    # generate 命令还可以带参数的
    ks generate spartakus spartakus --usageId=${usageId} --reportUsage=true
    ks generate application application
    

    真正通过 yaml 来创建 K8S 的资源的是在 kfctl.sh 脚本中,先通过同样的方法找出 ks 相关的命令。

    # 运行命令 cat kfctl.sh| grep "ks"
    # 这里指定 application 需要包括的组件
    # 上面提到了 applicaiton 是一个 crd,因为 kubeflow 
    # 的组件太多了,所以要有个工具统一管理
    ks param set application components '['$KUBEFLOW_COMPONENTS']'
    #
    #
    #
    # 下面是脚本里最后关键的步骤,请注意!!
    #
    #
    #
    # ks show 可以结合组件生成 yaml 文件
    ks show default -c metacontroller -c application > default.yaml
    # 然后可以看到,即使是这么复杂的 Kubeflow,依然是通过 kubectl apply 来构建
    # 所以有需要的话,一定要看看 default.yaml 文件
    # default 文件内容非常多,不同版本,应该在5000~9000行
    kubectl apply --validate=false -f default.yaml
    

    P.S. ks 命令没有全部列出,如需要 debug 来需要仔细看看脚本

    2.2 init 过程

    通过脚本 init

    ./kfctl.sh init myapp
    

    init 完之后,还要 check 版本的问题。

    image_1dhj704hu1etrlahjk1nagfrp60.png-235.5kB

    2.2 generate 过程

    # 注意目录
    cd myapp
    ../kfctl.sh generate all
    

    generate 之后,也同样 Check 版本信息。

    image_1dhj76kbg1to52tt1v9b1l111e216d.png-150kB

    2.3 apply 过程

    # 注意目录
    ../kfctl.sh apply all
    

    image_1dhj6o8uo1d778mp1083in013tt5j.png-145.4kB

    2.4 部署成功

    查看 pod 情况。

    image_1dhj7mo1p1llk1g1gdgv1s1h1s2u77.png-364.9kB

    查看 svc 情况。

    image_1dhj7obgkt0b1tjq1am51i7314k57k.png-277.9kB

    访问 UI。

    kubectl port-forward svc/ambassador 8003:80 -n kubeflow
    

    image_1dhj7rcj78ic13vun4j867167q81.png-151.9kB

    查看 Pipeline。

    image_1dhj7t2hk3kk1fk917qd1uve1hk58e.png-168kB

    运行一个 Pipeline 的 DAG。

    image_1dhj80ulg4e61omq6kf1geqdb898.png-57.5kB

    image_1dhj8b8581bt01a971js017fm18poa5.png-370.3kB

    查看 tf-job-dashboard。

    image_1dhj7u3b7176ft0qs5u11ckgoa8r.png-40.3kB

    提交一个 tf-job。通过官方提供的组件 example,里面有几个例子。可以通过以下方法安装这几个组件。

    # 注意需要目录在前面生成的 ks_app
    ks generate tf-job-simple-v1beta2 tf-job-simple-v1beta2
    ks apply default -c tf-job-simple-v1beta2
    

    这样就提交了几个任务了,本质上也是通过 ks 生成 yaml,然后 ks apply 就相当于 kubectl apply 了。

    image_1dhj8jvunku9mp212t8kdgo0cb2.png-109.9kB

    2.5 删除

    # 注意目录
    ../kfctl.sh delete all
    

    3 必须要注意的问题

    1. 一定要确认下载/安装 Kubeflow 的过程中,Kubeflow 的版本问题,因为其版本前后有比较大的差别!
    2. 生成模板的时候,需要注意 K8S 的版本!可以在脚本中指定,见附录。

    如果不打算部署整套 Kubeflow,可以只部署 Jupyter,tf-operator 等等。

    4 部署失败的原因

    1. 如果需要完整部署,需要创建多个 K8S 资源,需要较多的资源,本地不一定能部署起来,GCP 建议需要 16 核
    2. 版本问题,包括 K8S 版本,ksonnet 版本,镜像版本等等
    3. 离线问题,原则上,只要部署好 K8S 脚本,image 都在本地,部署脚本已经获取,是不需要联网部署的

    常见问题包括 Github 无法访问,需要下载 K8S 的 swagger.json 文件等等。

    完整部署一套 Kubeflow 代价太大,一是官方文档整理的逻辑不够清晰,更新不及时,二是包含太多组件了,如果对某些组件不熟悉,出问题了是很不好排查的。部署的话,最好是通过各云厂商的来部署,相对而言,Kubeflow 对各厂商的部署脚本的问题,处理起来比本地用户会更积极一些。当然了,在 GCP 上,体验应该是最好的。

    附录

    # ks 需要读取到 .kube/config 文件
    # init 需要确定 ks registry,离线安装需要 k8s 的 swagger.json
    eval ks init $(basename "${KUBEFLOW_KS_DIR}") --skip-default-registries --api-spec=file:/tmp/swagger.json
    # 
    # 可以指定 server,确定 k8s 的版本
    ks env add default --server=https://shmix1.k8s.so.db --api-spec=file:/tmp/swagger.json
    #
    # 注意每次运行脚本的信息
    ++ ks env describe default
    + O='name: default
    kubernetesversion: v1.14.3
    path: default
    destination:
      server: https://kubernetes.docker.internal:6443
      namespace: kubeflow
    targets: []
    libraries: {}'
    #
    #
    # 完整部署脚本
    #
    #
    export KUBEFLOW_VERSION=v0.5.0
    curl https://raw.githubusercontent.com/kubeflow/kubeflow/${KUBEFLOW_VERSION}/scripts/download.sh | sh
    cd scripts
    ./kfctl.sh init myapp
    cd myapp
    ../kfctl.sh generate all
    ../kfctl.sh apply all
    

    image_1dhj7gf34obd1ful361tdl5a86q.png-53.8kB

    展开全文
  • 一、前言 分布式大行其下的时代,让大家彻底的抛弃了传统陈旧的技术框架。...任务调度是指基于给定的时间点,给定的时间间隔或者给定执行次数自动的执行任务任务调度涉及到多线程并发、运行时间规则定制及解析
  • Android Gradle 本地化部署 maven

    千次阅读 2015-08-11 13:37:13
    1、介绍了 Android Gradle 部署 maven 到本地; 2、介绍了多 Module 互相引用时部署出问题的解决办法。
  • nodejs 应用部署

    千次阅读 2015-07-17 14:06:54
    很佩服芋头,但是他在杭州,我没法加入他的团队(估计我本身的水平也不够),就转载一篇文章,顺便当个收藏了。...“NodeJS在大搜车” 之 应用部署篇 07 JULY 2015 访问数:37 上次写了篇文章,有同学评价说
  • DevOps实践集——应用运维之持续部署 1. 场景 持续部署:业界没有统一明确地定义,简单理解为将集成结果部署到不同的环境供用户使用,并且立即反馈部署结果的实践,其中不同的环境包括:开发环境、...
  • 定时任务的选型及改造

    万次阅读 2018-03-24 23:18:39
    项目中用到了定时任务,项目之初为了快速开发上线,当时直接采用最简单的Linux自带的crontab;项目逐渐维定下来时,针对定时任务自己进行了相关研究,并根据项目实际情况进行了对比以及相关改造。 【比一比&改...
  • Maven系列(四)Maven热部署

    千次阅读 2016-06-06 12:17:16
    我所在的公司用的是...现有一个任务是将原先的项目发布改成maven热部署,也就是说发布到tomcat中后,不需要重启tomcat。 当我知道有这个功能的时候,内心是喜悦的,也明确了我今后要了解的只是 1、GitLab.CI 的原理
  • OpenStack大规模部署详解

    万次阅读 2017-07-07 09:08:52
    作者简介:付广平,云极星创研发工程师。 0.前言今年的2月22日,...OpenStack社区可能自己都没有想到其发展会如此之迅速,部署规模如此之大,以至于最开始对大规模OpenStack集群的部署支持以及持续可扩展性似乎...
  • spark部署模式解析

    千次阅读 2017-04-24 10:41:22
    单机上可以本地模式运行 ...standalone模式类似于单机伪分布式模式,如果是使用spark-shell交互运行spark任务或者使用run-example运行官方示例,driver是运行在master节点上的。如果使用spark-submit进行任务
  • spring schedule 配置多任务动态 cron 【增删启停】

    千次阅读 热门讨论 2021-03-13 21:47:38
    开发原则是: 在满足项目需求的情况下,尽量少的依赖其它框架,避免项目过于臃肿和复杂。主要研究spring 自带的schedule。 常见的任务调度方式 单机部署模式 Timer: jdk中自带的一个定时调度类,可以简单的实现按...
  • 微服务的设计原则

    千次阅读 2019-05-30 20:01:16
    每个微服务仅关注于完成一件任务并很好的完成该任务。那么关于微服务的设计原则有哪些呢?如下: AKF 拆分原则 前后端分离原则 无状态服务 RestFul 的通信风格 二AKF 拆分原则 有句挺流行的话:没有什么事是一顿...
  • SharePoint 部署基础知识 部署自定义 Web 部件 自定义站点标记 部署列表、窗体和报告 本文使用了以下技术: SharePoint目录 SharePoint 部署基础知识 Web 应用程序置备和设置 管理 Web.config 自定义 Web ...
  • OpenStack 部署运维实战

    千次阅读 2016-03-06 18:01:02
    OpenStack 部署运维实战 本文为您介绍了网易公司基于 OpenStack 开发的一套云计算管理平台,以及在开发、运营、维护过程中遇到的问题和经验分享。网易作为大型互联网公司,IT 基础架构需要支撑包括生产、...
  • 在 nginx 中部署 angular 应用

    万次阅读 2017-10-15 22:19:18
    最近使用Angular做了第一个应用,但是网上的教程大多是教如何开发,部署相对较少,所以这里就简单记录一下如何在nginx中部署Angular应用。 注:Angular应用可以编译成静态页面,然后部署在任何 web 服务器上,这里...
  • Docker持续部署图文详解

    千次阅读 2017-04-20 15:29:07
    都说Docker天生适合持续集成/持续部署,但同样,可落地、实际可操作性的文章也很罕见。 JAVA项目如何通过Docker实现持续部署(只需简单四步),即: 开发同学通过git push上传代码,经Git和Jenkins配合,自动完成...
  • Tomcat Web应用程序部署

    千次阅读 2012-06-04 00:37:17
    Tomcat Web应用程序部署 Introduction 部署是这个团队用于安装一个Web应用程序到Tomcat服务器的过程。 Web应用程序在Tomcat服务器的部署通常有两种方式. · 静态的; Web应用程序在...
  • 第六章 构建与部署的脚本化1. 引言要实现 自动构建 自动部署 构建和部署系统一直要保持活力,这个系统不仅要从项目开始就开发,而且一直持续到产品到上线维护阶段,细心设计和维护它,像对待项目源代码一样,并定期...
  • 最后一部分了。。。分两章吧。...本文粗粒度列出一些HBase部署运维的最佳实践和基本原则。 集群规划 一个完整的HBase集群包含HBase Master,ZooKeeper,RegionServers和Hadoop相关组件。生产集群按照规模
  • 在互联网的产品开发时代,产品迭代越来越频繁,“从功能开发完成直到成功部署”这一阶段被称为软件开发“最后一公里”。很多开发团队也越来越认识到,自动化测试和持续部署可帮助开发团队提高迭代效率和质量。 ...
  • "我鼓励你成为一个拥有三种美德的程序员:懒散,...使用Capistrano,一个ruby部署工具,可以让我们部署到远程服务器的工作变得简单,只需运行已定义好的任务即可。 Ruby程序员的工具箱包含很多可以减轻它们工作的工具
  • 由于最近爬虫用的服务器到期,需要换到新服务器重新部署,所以干脆把整个模块封装入Docker,以便后续能够方便快速的进行爬虫的部署。同时,由于我的Scrapy整合了redis,能够支持分布式爬取,Docker化后也更方便进行...
  • 架构设计的原则

    千次阅读 2016-12-07 20:39:32
    (1) N+1原则,即任何服务在部署时都要多部署一个冗余服务,这即能防止服务出现单点部署(由于N>1,所以部署总数至少为2),也能防止故障发生时系统还能有一定的承载的能力; (2) 回滚设计,确保每个升级的版本...
  • 这一篇文章主要讲基于Jenkins 快速搭建持续集成环境,但是我觉得其原理与我上篇文章是一样的:先从svn下载最新文件-->编译-->打war包-->部署到tomcat。而且上篇文章中的方式更加容易操作与理解,只是Jenkins提供的...
  • 分布式定时任务

    千次阅读 2019-08-02 16:46:10
    分布式定时任务 1,什么是分布式定时任务;2,为什么要采用分布式定时任务;3,怎么样设计实现一个分布式定时任务;4,当前比较流行的分布式定时任务框架; 1,什么是分布式定时任务: 首先,我们要了解计划...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 57,127
精华内容 22,850
关键字:

任务部署原则