博客

  • 异常日志

    异常日志

    日志是应用运行中被动或者主动输出,记录了应用相关信息的工具。

    日志一般有多个日志级别,包括TRACE、DEBUG、WARN、INFO、ERROR、FATAL。

    其中TRACE,DEBUG一般生产环境不输出,INFO属于一般信息,用于记录应用相关参数,运行状态等,WARN级别属于轻微的警告,用于一些不应该出现的或者轻微的错误,不影响应用。而ERROR和FATAL就是我们所说的异常了,这种级别的日志用于记录会影响业务进行和程序运行的情况。

    合理的输出异常本身对于应用的维护性有着很大的帮助。

    异常的类别

    要输出合理的异常信息,首先要明确有哪些异常,在我看来,异常无非有三种

    程序异常

    程序异常就是我们所说的真的挂了,也就是编程语言或者其运行环境自身提示异常,比如空指针异常。这种异常发生应用就已经出错的。这种错误不应该是开发人员手动输出的,而一般由公共的Exception Handler处理。这种异常理论上不应该直接输出,而是应该通过参数检查来避免,当检查不通过时应该转为输出可读的异常信息

    调用下游依赖异常

    现在巨型应用慢慢开始拆分,就出现了上下游调用的情况。比如一个财务系统,可能涉及到一些人员的信息展示,而这些信息可能由另外一个专门的人员服务来负责。那么当调用下游时就有可能出错,可能是返回不正确(null)也可能是超时,特别是互联网公司,很多超时都是200毫秒,网络抖动就会报错。

    业务报错

    对于任何输入都应该有验证,如果传入了错误的参数或者缺乏必要的参数,这种是逻辑上需要预处理的,而且技术上这种异常不是来自正常请求,因为调用方应该提供正确参数,若有问题应该在测试阶段暴露。

    还有一种报错来自应用框架。现代应用开发很少从底层弄上来,一般会使用一些框架,比如Spring Franework。这种框架自身会输出两种日志,一种是启动日志,一种是异常日志。

    比如Spring MVC在http请求缺乏必要参数时会抛出异常,这种异常大部分应该被开发人员手动转为有意义的异常。当然这个要看情况。

    异常日志的意义

    异常本身就代表了应用没有正常运行了,而异常日志的意义我个人觉得有几类:

    需要代码修复

    这种主要针对第一种异常,代码真的报错了,不管是什么原因,都代表这种情况可能出现,那么这里就需要人员去处理,处理之后重新上线。

    如果测试是充分的,那么这种异常一般是异常的调用或者异常的数据产生(异常不代表数据是错的,这里主要强调非正常数据,数量不大),那么这种问题尽快修复就行了。

    需要沟通

    这种主要是针对第二种异常,下游依赖出现问题的时候,那么首先要通知下游的服务提供方,它们可能有自己的监控,也可能没有,也可能这种异常本身没法被提供方的监控所感知,不管怎样,首先要让相关人员知晓情况,然后再说评估影响,作出数据修复或者继续观察。

    需要知晓

    有些异常可能只是让人心里有数,比如不正确的外部参数,这种如果自己的应用没有问题,那么一般都是不正常的请求来源,不需要提供正确的响应,不如说这种情况下的异常才是正确行为。

    但是这种情况都需要相关人员知晓,如果异常进一步扩大或者极速增加,那么还是需要采取一些防御手段的。

    合理的异常日志内容

    异常日志的内容要有合理有意义,主要是能够良好支撑我们对于日志的期望。日志框架的选择和收集方式我们这里不谈,主要说说日志的内容。

    互联网公司提供产品和服务给用户,通常这种服务是全天候的,根据业务的情况还可能出现非工作日业务量更大的情况,那么就需要有团队成员值班。而一个人应该了解自己团队所负责的服务和业务,但是对于每一个模块,每一行代码不可能都非常熟悉,那么异常日志就是提供一个快速判断,进而作出正确决策的输入来源。

    首先完整的堆栈信息是必要的,特别是对于程序异常,光跑出一个NullException但是没有具体的代码行数是没有意义的,完整的堆栈可以快速定位,这种情况的保证一般是框架自带的,偶尔也有手动输出的,只要不忘记传入exception就行了。

    logger.error("计算配送单{}时长失败", id, e);

    如果错误是框架统一异常处理的,一般会输出堆栈信息,比如Spring的DefaultErrorWebExceptionHandler

    对于下游调用异常的情况,首先为了方便沟通,需要输出的是调用方的信息,比如

    logger.error("调用人员服务超时,当前订单{}", id, e)

    上例的异常输出就可以快速判断下游所属,还有影响自己业务系统的范围。

    当然并不是所有异常都会直接影响系统功能,比如后台显示订单信息,在某些业务场景下,可以接受部分信息丢失,比如订单信息重要,但是涉及到的人员电话信息没有获取成功。那么这种情况日志中需要明确说明,对于核心业务没有影响。

    logger.error("调用人员服务失败,显示订单信息{}缺失人员电话",id , e)

    这样的日志很容易让人知道数据是不需要修复的,对于数据需要修复的,需要明示

    logger.error("调用骑手服务失败,无法写入骑手{}考勤数据,将影响奖惩数据", id, e);

    对于不同的服务还要注意输出的时机,比如对外提供HTTP API的服务,对于一些错误,我们可能返回的依然是200状态码,但是其中包含了错误信息,这种错误返回并不会被监控系统自动捕获,这种情况可以选择手动输出异常,然后再返回响应给前端。

    以上是比较合理的输出,还有一些比较常见不合理的输出方式,比如二次抛出同样的错误

    catch (NoRiderException e) {
           logger.error("No rider id: {} available",id , e);
           throw new UserServiceException("Nouseravailable", e);
    }

    我还注意到网易技术团队的博客提到了一种模版方法,就是任何一个异常,都尝试填充这个模版,从团队统一规范的层面保证异常日志的输出。详细博客参考这里。模版如下:

    log.error(“[接口名或操作名] [Some Error Msg] happens. [Probably Because]. [Probably need to do]   [params] .”);
    log.error(“[接口名或操作名] [Some Error Msg] happens. [Probably Because]. [please contact xxx@xxx]   [params] .”);

    我个人觉得接口信息应该在堆栈中就有,而联系人信息一般到组就行了,如果实在需要邮箱组这种联系方式,还是用一个经常维护的团队邮件组比较好,毕竟存在人员流动的情况,也有组织变动的情况。

    写在最后

    总的来说异常的输出其实是应用可维护性还有人员对于系统理解程度的体现,什么异常对于系统有影响,什么异常是致命,写下相关代码的人最清楚。合理的异常日志能极大帮助相关人员快速定位问题,减少在线错误报警,另一方面也是减轻开发人员的工作量。

  • Docker化CAT监控系统

    CAT是点评开源的监控系统,可以统计到粒度非常细,而且也支持hadoop,可以应对企业应用。

    CAT底层使用的是MySQL,需要预先初始化数据库,而且还附带了几个配置文件,总之并不是一个war包就可以启动的。

    为了本地快速调试和集成,还是Docker化一下吧。为了简单起见就只准备一个单机模式的镜像。

    首先是准备几个xml文件,因为是单机模式,就全部只想要127.0.0.1了。

    <?xml version="1.0" encoding="utf-8"?>
    
    <config mode="client" xmlns:xsi="http://www.w3.org/2001/XMLSchema" xsi:noNamespaceSchemaLocation="config.xsd" xmlns="">
        <servers>
            <server ip="127.0.0.1" port="2280" http-port="8080"/>
        </servers>
    </config>
    <?xml version="1.0" encoding="utf-8"?>
    
    <config local-mode="false" hdfs-machine="false" job-machine="true" alert-machine="false">
    
        <storage local-base-dir="/data/appdatas/cat/bucket/" max-hdfs-storage-time="15" local-report-storage-time="7"
                 local-logivew-storage-time="7">
        </storage>
    
        <console default-domain="Cat" show-cat-domain="true">
            <remote-servers>127.0.0.1:8080</remote-servers>
        </console>
    </config>

    最关键的就是数据库的准备了,CAT仓库准备了一个SQL文件。我的操作是在连接数据库的时候先检查数据库是否有表,没有就先建表。之后再替换datasource配置中的文件信息

    #!/usr/bin/env bash
    
    RESULT=`mysqlshow -u ${DATABASE_USER} --password=${DATABASE_PASSWORD} --port 3306 --host ${DATABASE_HOST} cat | grep -v Wildcard | grep -o dailygraph`
    if [ "$RESULT" != "dailygraph" ]; then
        mysql -u ${DATABASE_USER} --password=${DATABASE_PASSWORD} --port 3306 --host ${DATABASE_HOST} cat < /app/Cat.sql
        echo 'Init database from Cat.sql'
    fi
    
    sed -i "s/jdbcHost/$DATABASE_HOST/g" /data/appdatas/cat/datasources.xml
    sed -i "s/jdbcUser/$DATABASE_USER/g" /data/appdatas/cat/datasources.xml
    sed -i "s/jdbcPassword/$DATABASE_PASSWORD/g" /data/appdatas/cat/datasources.xml
    
    catalina.sh run

    最终的使用方法如下:

    version: '3'
    
    services:
      database:
        image: mysql:5
        environment:
          - MYSQL_ROOT_PASSWORD=0
          - MYSQL_DATABASE=cat
      cat:
        image: htynkn/dianping-cat
        depends_on:
          - database
        ports:
          - 8080
        environment:
          - DATABASE_USER=root
          - DATABASE_HOST=database
          - DATABASE_PASSWORD=0

    如果要快速体验的话可以用PWD: http://play-with-docker.com/?stack=https://raw.githubusercontent.com/htynkn/dockerfiles/master/dianping-cat/stack.yml

    界面截图如下:

     

  • DataGrip的自动代码生成

    DataGrip的自动代码生成

    DataGrip

    DataGrip是一款数据库客户端工具,是大名鼎鼎的JetBrains公司出品的,相信大部分程序员都用过同公司的Intellij IDEA。

    DataGrip可以连接到数据库服务器,执行sql、管理表(创建表,修改索引等等)以及导出数据等。之前一直用的免费的MySQL Workbench,这个工具是免费的,能够满足日常需求,但是在Mac上经常死机,还有就是自动感应不是很灵敏。另外一个缺陷是专为MySQL设计。

    想着因为是Apache committer的原因Jetbrains的全线产品免费,就尝试了一下DataGrip。

    DataGrip和Intellij IDEA是一个壳子的,所以从样式,布局都是一样的,熟悉感很重,特别是两个软件切换来切换去的时候特别容易搞混。

    DataGrip对于数据库的支持很宽泛,从PostgreSQL到MySQL再到Oracle这些都支持(其实我平时只用到了MySQL)。虽然官方没有说支持Hive数据库,但是实际上可以自定义driver和datasource,也可以用。

    代码自动生成

    这里的代码自动生成就是指数据库相关的Java代码或者MyBatis mapper可以自动生成。

    代码自动生成对于效率的提升是很大的,不光是时间上的节约,还有一些就是代码准确度的提升。从纯工程实践的角度讲,给予成熟模版生成的代码绝对是一个很划算的买卖。特别对于一些完全没有技术含量的活,代码自动生成尤为重要。

    现在很多代码自动生成都是基于Velocity 模板引擎来做的,也就是说代码的情况完全看模版,如果有需要可以修改模版(前提是工具或者扩展允许修改)。

    Mybatis自带生成器,也有maven插件,但是灵活性一般,而且配置比较繁琐。

    DataGrip的脚本扩展

    DataGrip在2017.01版开始提供脚本扩展,支持groovy和cli。用法很直接,选中一个表,右键执行对应的脚本就行了。

    网上能找到一些脚本,但是更多时候需要自己修改以符合公司内部规范或者项目组的一般规范,不过脚本扩展的优势就是比较容易修改,不过可读性其实比不过模板生成的模式。

    DataGrip自带了一个生成Pojo的groovy脚本,可以直接用,不过那个脚本不会自动添加注释,自己稍加修改就好了。

    脚本扩展的例子

    import com.intellij.database.model.DasTable
    import com.intellij.database.model.ObjectKind
    import com.intellij.database.util.Case
    import com.intellij.database.util.DasUtil
    
    /*
     * Available context bindings:
     *   SELECTION   Iterable<DasObject>
     *   PROJECT     project
     *   FILES       files helper
     */
    
    packageName = "com.sample;"
    typeMapping = [
      (~/(?i)bigint/)                      : "Long",
      (~/(?i)int/)                      : "Integer",
      (~/(?i)float|double|decimal|real/): "java.math.BigDecimal",
      (~/(?i)datetime|timestamp/)       : "java.util.Date",
      (~/(?i)date/)                     : "java.util.Date",
      (~/(?i)time/)                     : "java.util.Date",
      (~/(?i)/)                         : "String"
    ]
    
    FILES.chooseDirectoryAndSave("Choose directory", "Choose where to store generated files") { dir ->
      SELECTION.filter { it instanceof DasTable && it.getKind() == ObjectKind.TABLE }.each { generate(it, dir) }
    }
    
    def generate(table, dir) {
      def className = javaName(table.getName(), true)
      def fields = calcFields(table)
      new File(dir, className + ".java").withPrintWriter { out -> generate(out, className, fields) }
    }
    
    def generate(out, className, fields) {
      out.println "package $packageName"
      out.println ""
      out.println ""
      out.println "public class $className implements java.io.Serializable{"
      out.println ""
      fields.each() {
        out.println ""
        out.println "  /**"
        out.println "   * ${it.comment}"
        out.println "   */"
        if (it.annos != "") out.println "  ${it.annos}"
        out.println "  private ${it.type} ${it.name};"
      }
      out.println ""
      fields.each() {
        out.println ""
        out.println "  public ${it.type} get${it.name.capitalize()}() {"
        out.println "    return ${it.name};"
        out.println "  }"
        out.println ""
        out.println "  public void set${it.name.capitalize()}(${it.type} ${it.name}) {"
        out.println "    this.${it.name} = ${it.name};"
        out.println "  }"
        out.println ""
      }
      out.println "}"
    }
    
    def calcFields(table) {
      DasUtil.getColumns(table).reduce([]) { fields, col ->
        def commentStr = col.getComment()
        def spec = Case.LOWER.apply(col.getDataType().getSpecification())
        def typeStr = typeMapping.find { p, t -> p.matcher(spec).find() }.value
        fields += [[
                     name : javaName(col.getName(), false),
                     type : typeStr,
                     annos: "",
                     comment : commentStr]]
      }
    }
    
    def javaName(str, capitalize) {
      def s = com.intellij.psi.codeStyle.NameUtil.splitNameIntoWords(str)
        .collect { Case.LOWER.apply(it).capitalize() }
        .join("")
        .replaceAll(/[^\p{javaJavaIdentifierPart}[_]]/, "_")
      capitalize || s.length() == 1? s : Case.LOWER.apply(s[0]) + s[1..-1]
    }

     

    参考

    https://www.jetbrains.com/datagrip/

    https://www.jetbrains.com/help/datagrip/2017.1/extending-the-datagrip-functionality.html?search=extending

    http://www.mvnrepository.com/artifact/org.apache.hive/hive-jdbc/3.1.0

    https://www.ibm.com/developerworks/cn/java/j-lo-velocity1/index.html

  • 本地搭建hive集群

    最近有一些hive相关的工作,之前只用过简单的hive,现在只有赶快补一下。

    本地还是用docker启一个hive集群,我直接用的https://github.com/big-data-europe/docker-hive.git 的脚本,clone下来以后直接docker-compose up -d

    查看8080端口可以看到集群状态

    数据库端口在10000上。

    这里我用DBVisualizer来连接一下。

    先下载Hive Driver

    然后添加连接hive://localhost:10000

     

  • 使用Docker快速启动一个Spark集群

    docker-compose 文件如下

    version: '3'
    services:
      master:
        image: bde2020/spark-master:2.3.1-hadoop2.7
        ports:
          - "8080:8080"
          - "7077:7077"
          - "6066:6066"
        environment:
          - INIT_DAEMON_STEP=setup_spark
      worker:
        image: bde2020/spark-worker:2.3.1-hadoop2.7
        depends_on:
          - master
        ports:
          - "8081"
        environment:
          - "SPARK_MASTER=spark://master:7077"
    

     

    然后运行docker-compose up -d –scale worker=3 就可以启动一个有三个节点的集群了.

  • 细说降级

    什么是降级

    当出现大流量或者其他情况时我们需要保护我们的系统,这里有三大利器:缓存、降级和限流。

    降级是一种利用有限资源,保障系统核心功能高可用、有损的架构方法。降级的最终目的是保证核心服务可用,即使是有损的,同时有些服务是无法降级的。

    实施降级

    实施降级需要综合分析系统情况和业务情况,要判断是否可以“丢卒保帅”,同时梳理出核心链路,保护核心业务。

    考虑到影响业务的范围降级可以分为以下几种:

    页面降级

    在特殊情况下(一般是热门活动),有些页面占用了一些稀缺服务资源,在紧急情况下可以对其整个降级,页面暂时无法访问。

    页面片段降级

    比如商品页面中包含其他商品推荐信息,这个片段可以暂时屏蔽。

    页面异步请求降级

    比如商品详情页上有推荐信息/配送至等异步加载的请求,如果这些信息响应慢或者后端服务有问题,可以进行降级

    服务功能降级

    比如渲染商品详情页时需要调用一些不太重要的服务,比如外卖中的骑手配送路径,紧急情况下可以不显示路径,只显示配送时间和状态。

    读降级

    比如多级缓存模式,如果后端服务有问题,可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景;

    写降级

    特殊情况下如果可以接收最终一致性,可以直接写cache,用于缓解数据库的压力,比如秒杀。

    降级预案

    一般情况下降级的发生是可以预期的,比如相关业务促销,或者新业务进入新城市的时候。这个时候预案是最重要的,首先再次明确核心链路是什么,哪些不能超时,不能不可用,哪些可以接收一定超时,不能不可用,哪些可以超时,可以不可用。

    我们假象一个微信公众号点餐系统,比如下图

    很容易梳理出来查看菜品,余额和消费付款都是核心,会直接影响交易本身。而会员信息修改,发放优惠券就是暂时不可用的,而菜品评价是可以部分可用或者接收一定的超时的。

    梳理出这些内容以后,那么就需要明确降级开关的触发条件和方式,这个是业务和技术的综合问题,同时要明确降级触发以后的需要周知的干系人。

    自动降级

    监控对于系统的健康必不可少,监控可以覆盖很多方面,比如流量,JVM,日志等等。而自动降级是一个很吸引人的东西,它是根据系统负载、资源使用情况、SLA等指标自主进行降级。

    超时降级

    当访问的数据库或者rpc调用超过默认设定值以后,若服务不是核心服务的话可以在超时后自动降级;具体的服务响应最大时间需要和服务提供方商议,或者根据监控系统的历史数据来评估。

    失败次数降级

    有时候依赖一些不稳定的API,比如调用第三方商家系统,当失败调用次数达到一定阀值后自动降级;然后通过异步线程去探测服务是否恢复了,则取消降级。

    这个有点类似半开式熔断。

    故障降级

    比如要调用的远程服务挂掉了比如Dns故障,则可以直接降级。降级后的处理方案有:兜底数据、缓存或者默认值。

    限流降级

    当我们去秒杀或者抢购一些限购商品时(比如小米抢购…),此时可能会因为访问量太大而导致系统崩溃,可以用限流来限制访问量,当达到限流阀值,后续请求会被降级;降级后的处理方案可以是:排队页面。

    参考

    http://jinnianshilongnian.iteye.com/blog/2306477

  • 权限控制与模型

    我们在做任何一款产品的时候,或多或少都会涉及到用户和权限的问题。比如企业软件,不同的部门,不同的岗位都需要不同的权限控制;再比如CMS系统,那也有管理员,内容审核等等;

    产品权限控制部分的好坏直接影响了产品的可扩展性和后期维护性,当然市面上也有很多成体系的权限控制方案,比如你可以用Apache Shiro。

    这里抛开框架,我们来看看最简单粗暴的方案吧

    用户等于权限

    简单粗暴并不代表它不能用,比如在一个权限极为简单的系统,假设只是管理员和普通用户,我们完全可以硬编码管理员的用户名在代码中,然后一个if-else就可以解决问题。

    当然为了后期的灵活性,而我们管理员又经常改变,那么我们在表中额外加一个字段,通过这个字段来表示用户是否为管理员。

    ACL

    用户直接关联权限对于大规模的应用比较难维护,而且粒度比较大。ACL可以很好解决粒度问题,因为它对于每一个资源都定义了一个权限控制表,当然灵活性那是很高了,但是维护成本太高,而且大规模使用性能也很难保证。

    RBAC0

    用户等于权限这种映射和管理太粗旷了,在稍微复杂一点的情况下就很难使用了,比如有两种管理员,权限有大有小,甚至都不是子集关系。
    RBAC0就进了一步,将权限和角色捆绑在一起,而用户和角色联系在一起。因为用户和角色是多对多,而角色和权限也是多对多,那么整体来看可以实现的控制灵活性和粒度就非常好了,而用户拥有的权限等于他所有的角色持有权限之和。

    这种的好处是权限的聚合,比如抽象出了财务专员和人事专员,那么老板很可能就是单纯拥有这两个角色而已。

    RBAC1

    在RBAC0的基础上引入分层概念,就是RBAC1了,也就是带继承的模型。比如财务专员和财务负责人,那么财务负责人其实是有财务专员的所有权限的。RBAC1并不是增加了整体的灵活性,但是简化了权限管理。

    RBAC2

    RBAC2是限制模型,虽然是RBAC2,但是它和RBAC1没啥关系,而是在RBAC0的基础上增加了限制条件。用户的权限取决于它激活的角色所有的权限,同时角色还不能拥有互斥的角色,比如一个人不能又发起合同,又审批合同。
    同时RBAC2还要求角色之间是有依赖的,比如要激活财务负责人的角色,必须拥有公司员工这个角色。

    当然这些限制有一套专业名词:静态职责分离和动态职责分离

    RBAC3

    RBAC3是RBAC1和RBAC2的集合,即同时拥有角色分层和限制管理。

    ABAC

    基于属性的权限验证是比RBAC新一些的概念和模型抽象, 之前说的所有模型都是将用户通过某种方式关联到权限的方式,唯一的不同就是怎么关联的。

    而ABAC则是通过动态计算一个或一组属性来是否满足某种条件来进行授权判断。属性通常来说分为四类:用户属性,环境属性,操作属性和对象属性,所以理论上能够实现非常灵活的权限控制,几乎能满足所有类型的需求。

    当然高度的灵活性就意味者管理上的复杂性,实际上为了达到ABAC的灵活动态,它需要专门的配置和动态引擎来解析处理,具体可以查看XACML。

     

    大部分情况下我觉得RBAC簇的抽象就足够了,毕竟大部分产品的权限还是基于社会分工的,也就是职位职务的,其实RBAC的抽象是非常贴近现实的。

  • 查询Github star历史

    查询Github star历史

    Github上的star是一个很重要的指标,特别是你想用新的库到项目中或者自己想学习的时候。

    今天突然找到一个在线版的工具,它通过访问github的stargazers api来获取star历史,然后绘图展现。有需要可以用一下 http://www.timqian.com/star-history

    效果如下:

  • Google的jib打包工具

    Google的jib打包工具

    今天看群里说起jib工具,就专门看了一下。地址:https://github.com/GoogleContainerTools/jib。

    jib旨在让开发者使用他们熟悉的工具更轻松地将 Java 应用程序容器化。

    来看看一般的应用如何容器化

    1. 编译构建出jar包或者war包
    2. 编写Dockerfile
    3. Docker 构建镜像到本地或者发布到仓库

    第一步还好说,构建本身由于maven和gradle的存在变得相当便利,如果是spring boot的应用,直接打包jar包,其他的用war插件打包war包就行了。

    第二步编写Dockerfile,大部分Dockerfile的内容都是相似的,准备对应的java环境,拷贝jar或者war,然后配置参数,指定启动脚本。

    第三步也很直接,直接执行docker build . -f Dockerfile

    那么Google专门开源的这个jib工具能够改善的点在何处呢?

    看看Google官方的说明:

    1. 简单 – Jib 采用 Java 实现,并作为 Maven 或 Gradle 构建的一部分运行。你不需要维护 Dockerfile ,甚至无需创建包含所有依赖项的 JAR 包。
    2. 快速 – Jib 利用镜像分层和注册表缓存来实现快速、增量构建。它读取你的构建配置,将应用分到不同的层中,只重新构建和推送发生变更的层。
    3. 可重现 – Jib 支持根据 Maven 和 Gradle 的构建元数据进行声明式的容器镜像构建,只要输入保持不变,就可以通过配置重复创建相同的镜像。

    第一个优势就是干掉了Dockerfile,第三个优势类似于jar包和war包本身没有明显的版本信息,重现性低。

    那么第二个快速应该就是一个主要优势了,之前的Docker打包是以整个jar包或者war包而基础的,那么每个Docker镜像的拉取量就是整个jar包或者war包,而jib的打包是基于层的,具体的步骤还是有点复杂,官网有专门的文档说明,查看下面两个链接:

    https://github.com/GoogleContainerTools/distroless

    https://cloudplatform.googleblog.com/2018/07/introducing-jib-build-java-docker-images-better.html

  • Play With Docker

    有时候网上找到了一个docker镜像,可能需要快速了解一下,拉到本地,然后配置环境启动有时候有点慢。这种情况Play With Docker就是一个不错的选择,地址:https://labs.play-with-docker.com/

    如果你是镜像的维护者,还可以提供一个stack文件地址,比如 http://play-with-docker.com/?stack=https://raw.githubusercontent.com/htynkn/dockerfiles/master/dubbo-admin/stack.yml

    这样可以快速体验一把,这里的例子是dubbo-admin