博客

  • 使用JGit获取变更细节

    有时候多个项目在一个代码仓库中,可能每个文件夹都是一个项目。如果每次使用CI自动部署的话就需要判断最近的改动属于哪个文件夹/项目。

    通过JGit直接获取两个commit之间的diff然后判断是比较直接的方法,具体使用哪两个commit就看需要的。可以使用CI系统上一次成功构建的commit sha,也可以直接使用最近两天的commit的。

    代码如下:

    Repository repo = FileRepositoryBuilder.create(new File(basePath + ".git"));
    Git git = new Git(repo);
    ObjectReader reader = repo.newObjectReader();
    ObjectId newObjectId = repo.resolve("HEAD^{tree}");
    ObjectId oldObjectId = repo.resolve(successSha + "^{tree}");
    
    
    System.out.println("Starting diff " + oldObjectId + " : " + newObjectId);
    
    CanonicalTreeParser oldTreeIter = new CanonicalTreeParser();
    oldTreeIter.reset(reader, oldObjectId);
    CanonicalTreeParser newTreeIter = new CanonicalTreeParser();
    newTreeIter.reset(reader, newObjectId);
    
    List<DiffEntry> diffs = git.diff().setNewTree(newTreeIter).setOldTree(oldTreeIter).call();
    
    for (DiffEntry entry : diffs) {
        System.out.println("Entry: " + entry);
    
        for (String folderName : folderNames) {
            if (entry.getNewPath().contains(folderName) || entry.getOldPath().contains(folderName)) {
                System.out.println("Need deploy folder: " + folderName);
                foldersNeedDeploy.add(folderName);
            }
        }
    
    }

    其中的successSha是上一次成功部署过的commit sha值,也可以使用类似HEAD^^^^^的表达式直接使用倒数第四次commit。

    如果是Groovy脚本的话需要引入依赖

    @Grapes([
            @Grab(group = 'org.eclipse.jgit', module = 'org.eclipse.jgit', version = '5.2.1.201812262042-r')
    ])
  • 压测前的影子表预热

    影子表和buffer pool

    影子表并不是一个数据库的特殊功能,简单来说只是和原表结构一致但是名字不一样的普通表而已。所以创建影子表并不需要特别的操作,根据数据库中间件的支持规则创建就行了。

    buffer pool是一个缓存数据和索引信息到内存的存储区,简单理解就是一个热数据区,或者叫缓存。具体的配置和计算比较复杂,但是总体而言,buffer pool命中率越高,性能越好。

    预热的价值

    因为影子表的数据是冷数据,这一部分数据并不在buffer pool中,需要逐步加载到内存中,因此需要预热过程。如果没有预热,会出现性能问题,但是这一部分并不是我们期待的,因为线上buffer pool中是包含了真实数据的。

    执行预热

    执行预热就是低流量的压测,让影子表的数据被访问到,并加载到buffer pool中。

    其次就是执行预热前,需要先dump出线上的buffer pool,然后在压测完成重新load之前导出的buffer pool。

    其他要点

    预热的时候要注意因为buffer pool的空间是公用的,要注意不要让影子表的数据影响线上数据了。业务高峰期很难成功,因为线上的流量大,真实数据占用buffer pool的量比较大,预热数据的buffer很快就被线上请求踢出。

    预热时要密切观察慢查询的量,有问题就需要降速或者停止,严重的情况还需要在DB上kill查询,并重新加载buffer pool。

  • 内联第三方依赖到自己的包中

    Java世界一个很难绕过的问题就是依赖包,绝大部分项目总会或多或少的依赖第三方包。Maven或者Gradle等可以帮助我们轻松的管理第三方依赖,但是对于自己对外提供的包,特别是作为中间件或者基础组件等提供出去的包,还是会遇到一些第三方依赖的问题,主要有两个问题

    • 包依赖比较少,而且依赖第三方包中的极少部分代码
    • 一些关键依赖版本敏感,不希望使用方随意变更

    第一个情况可以先打一个fatjar,然后通过ProGuard精简代码。

    第二个情况就需要把第三方包的代码拷贝到自己的包中,在自己的代码中直接使用拷贝进来的三方包代码(pakcage name不一样),这样就可以最大程度保证稳定性。

    当然大部分情况下对于第三方包的版本依赖没有那么敏感,上面说到的情况其实是大部分针对asm,cglib和objenesis等几个字节码增强的包。

    具体的操作可以借助jarjar工具,先将三方包重新打包,然后修改Gradle的Jar任务,将重新打包后的jar复制到自己的包中。

    task cglibRepackJar(type: Jar) { repackJar ->
        repackJar.baseName = "cglib-repack" repackJar.version = cglibVersion doLast() {
            project.ant {
                taskdef name: "jarjar", classname: "org.pantsbuild.jarjar.JarJarTask", classpath: configurations.jarjar.asPath
                jarjar(destfile: repackJar.archivePath) {
                    configurations.cglib.each { originalJar -> zipfileset(src: originalJar) }
                    rule(pattern: "net.sf.cglib.**", result: "com.huangyunkun.cglib.@1")
                    rule(pattern: "org.objectweb.asm.**", result: "com.huangyunkun.asm.@1")
                }
            }
        }
    } 

    然后修改Gradle的Jar任务配置

    jar {
    dependsOn cglibRepackJar
    from(zipTree(cglibRepackJar.archivePath)) {
    include "com/huangyunkun/cglib/**"
    }
    }

    当然在具体打包的时候还可以根据需要exclude一些不需要的模块。

    参考:https://github.com/shevek/jarjar

  • 全链路压测改造

    全链路压测是基于实际的生产业务场景、系统环境,模拟海量的用户请求和数据对整个业务链进行压力测试,并持续调优的过程。

    全链路压测针对是线上真实环境,主要的目的是针对业务场景越发复杂化、海量数据冲击下整个业务系统链的可用性、服务能力的瓶颈,让技术更好的服务业务,创造更多的价值。简单来说就是为了快速准确找出系统的优化点。

    全链路压测听起来很好,各个大厂都可以找到相关的博客或者分享文章,但是要实施全链路压测需要很大的工作量,对于系统需要进行改造。

    由于全链路压测涉及整个技术生态,不同的公司使用的技术和基础设施各不相同,所以改造内容也不同,但是大致的改造是类似的,主要在以下几个方面:

    流量标记

    流量标记的作用是让所有服务或者说是代码在任何一个时刻都能知道当前是否为压测。因为实际业务中链路一般都比较长,从头开发这样的一个功能还是比较复杂。

    目前大部分系统为了全链路追踪一般都是用了OpenTracing标准的某种实现,那么在OpenTracing中的Span中的Context就是一个绝好的信息载体。

    对于对外暴露的HTTP API请求,需要从URL中获取到压测信息,然后将其传递到RPC或者其他调用层,RPC层依次传递对应的标志就行了。

    RPC框架

    RPC上的改动比较上,主要是需要用上下文传递压测标记。这里可以用RPC框架自带的上下文,当然用OpenTracing的比较方便,这一步改动不大。

    数据库中间件

    既然要做全链路压测,那么肯定不能只测试读接口,对于写接口,需要考虑到数据隔离的问题。

    这个时候影子表是比较合适的选项,影子表是与真实表对应的一个概念,与真实表同构,只是名称不同。在MySQL的innodb引擎中,影子表与真实表存储的文件不同,但是共用buffer pool。总体而言,影子表的使用是安全,能够有效模拟真实表,同时又能起到逻辑隔离的效果:

    数据库中间件需要支持的是影子表偏移的,而且还需要支持配置,因为根据业务和系统情况,并不是所有表都需要影子表,而且影子表的偏移各不相同。

    其次就是和影子表有关的其他改造,比如初始化,预热,buffer pool恢复,数据构造和数据清理等都是需要考虑的。

    MQ中间件

    MQ中间件主要是两个问题,一个是压侧标识传递,另外一个是队列隔离。

    日志组件

    日志组件上主要是压测流量日志是否需要打印特定标识,是否需要单独的降级开关。

    线程池

    业务系统或多或少用到了线程池,而线程池一般都需要改造用于支持压测标识的传递。

    Mock

    一个完整的业务流程很难只和自己的系统打交道,外部系统一般不纳入压测中,比如第三方保险/语音电话服务/短信等等,有时候也会遇到下游系统并没有准备好加入压测链路,所以需要Mock。

    具体的Mock返回数据要根据情况创造。

    降级

    降级并不是全链路压测必须的,但是提供降级开关可以在压测出现问题的时候中断部分接口,依然执行全链路压测,以免压测失败。

  • 快速搭建本地OpenWhisk环境

    快速搭建本地OpenWhisk环境

    OpenWhisk是IBM开源的FAAS平台。OpenWhisk 简化了微服务的部署,消除了管理自己的消息代理或部署自己的工作服务器的需求。

    有时候为了方便本地开发验证,需要搭建一个OpenWhisk环境,但是OpenWhisk依赖多,配置比较复杂,要从头搭建一个还是比较繁琐。

    官方提供了快速搭建工具,可以快速启动一整套环境来

    git clone --depth=1 https://github.com/apache/incubator-openwhisk-devtools.git
    
    cd incubator-openwhisk-devtools/docker-compose
    
    make quick-start

    会基于Docker启动openwhisk/controller,openwhisk/invoker,zookeeper,kafka,redis等组件。

  • MySQL下的CommunicationsException问题

    这周的一天早上一个定时任务突然报错,错误如下

    ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure The last packet successfully received from the server was 67,409 milliseconds ago. The last packet sent successfully to the server was 60,060 milliseconds ago. ### The error may exist in file [xxxxx.xml] ### The error may involve defaultParameterMap ### The error occurred while setting parameters 

    字面上看是数据库链接出了问题。

    最近才把应用的数据库链接池从tomcat切换到druid,所以第一个怀疑的就是druid。

    网上搜索搜索一下,发现大部分答案都基本是在连接取出之前需要进行测试,连接池一般支持这样的配置。稍微检查了一下,发现项目中已经配置了相关的配置,所以理论上不是这个问题导致的。

    然后仔细研究了一下这个问题,发现这个问题其实蛮常见的,错误信息是一样的,但是具体对应的情况却是各不相同的。比如上文提到的是由于连接超时连接池再取出连接前没有进行测试,这只是一种情况,还有一种比较常见的情况是连接已经建立,但是由于慢查询或者服务器压力过大,导致长时间没有数据返回,占用数据库连接,进而达到数据库最大允许的连接数。

    根据这个思路检查了一下数据库平台,从监控中可以看到有一个慢查询持续了60余秒,优化掉这个慢查询就解决了这个问题。

    当然还有一些其他比较基本的情况,比如数据库地址错误,导致直接无法连接,或者是数据库配置有误,导致无法接受连接,不过这种问题一般出现在初次连接的时候,一般不会发生,在稳定运行的线上环境。

  • ubuntu快速安装docker

    最近换了一个硬盘,重新装了一下系统。
    自然各种环境和工具也要重新装。

    apt install没法安装docker-ce,官方文档虽然详细,但是步骤挺多的。然后就发现官方的安装脚本,还支持镜像。

    curl -fsSL get.docker.com -o get-docker.sh

    sudo sh get-docker.sh –mirror Aliyun

  • Gatling使用的几个小技巧

    Gatling是一个基于Scala,Akka和Netty的开源负载和性能测试框架,从功能上和JMeter很类似,只不过因为是基于DSL书写,从配置上来说灵活性比较好,当然相对的上手难度其实要大一些。

    最近项目上使用了一下Gatling,用的时候还是遇到一些小问题。

    Gradle插件运行

    Gatling官网的运行方式是下载整个Gatling,然后编写脚本以后,直接运行。这个操作不是很方便的,找到一个Gradle插件可以快速运行,其中Gatling的版本,参数等都可以通过build.gradle的参数配置,使用上是比较灵活的。

    插件的配置如下:

    plugins {
        id 'scala'
        id "com.github.lkishalmi.gatling" version "3.0.2"
    }
    
    gatling {
        toolVersion = '3.0.0'
        jvmArgs = [ '-server', '-Xms512M', '-Xmx512M' ]
        simulations = {
            include "**/*.scala"
        }
    }

    目录结构的要求是src/gatling/simulations,这个需要自己改一下

    读取配置

    配置可以是很多东西,比如比较常见的是不同环境不同的数据,比如URL,用户认证信息,或者是压测的压力情况都应该在配置,而不是硬编码在代码中。

    配置文件地址src/galting/resource/application.properties

    使用的时候直接调用

    val conf = ConfigFactory.load()
    val httpConf = http.baseUrl(conf.getString("baseUrl"))
      ...
      ...

    配置的内容可以文本,也可以是数字等。

    参数传递

    有时候测试一些API需要有关联,会存在一些参数传递。比如首先调用创建API,获取返回值中的id,然后测试修改API,需要在请求数据中包含该id。

    Gatling提供的方法是session,可以自己写入,也可以从返回中提取。

    val xx = scenario("XX管理")
        .exec(
          http("新建XX")
            .post("/xx/create")
            .check(status.is(200))
            .check(jsonPath("$.code").is("0"))
            .check(jsonPath("$.data.id").saveAs("xxId"))
        )
        .exec(
          http("更新XX")
            .post("/xx/update")
            .formParam("id", session => session("xxId").as[Int])
            .check(status.is(200))
            .check(jsonPath("$.code").is("0"))
        )

    然后需要把session中的值注入请求中,而请求比较大,是存放在文件中的,可以使用ElFileBody

    .exec { session => session.set("title", "标题" + ThreadLocalRandom.current.nextInt(10000)) }
      .exec(
        http("添加内容")
          .post("/content/save")
          .body(ElFileBody("json/save.json")).asJson
          .check(status.is(200))
      )

    用户配置

    Gatling可以指定模拟的用户量,比较简单的是atOnceUsers,这是一次性模拟一定数量的用户,但是很多时候我们需要一个缓慢增长的过程,比如用户在3秒中从1增加到100

    setUp(
        scn.inject(
            rampUsers(10) over (5 seconds), 
            constantUsersPerSec(20) during (15 seconds), 
            rampUsersPerSec(10) to 20 during (10 minutes), 
            splitUsers(1000) into (rampUsers(10) over (10 seconds)) separatedBy (10 seconds),
            heavisideUsers(1000) over (20 seconds)
        ).protocols(httpConf)
    )

    具体用哪种看情况,但是一定要引入如下的包,不然seconds这些时间控制会一直报错

    import scala.concurrent.duration._

    Gatling测试结果

    Gatling的默认测试结果是一个网页,可视化效果比起来JMeter要方便不上,但是有时候需要自定义的情况,可以直接从Gatling的结果文件中进行加工。以下是一些加工效果

  • MySQL索引

    MySQL索引

    MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在各大互联网公司中,MySQL即其变种是最好的使用最多的关系型数据库。

    MySQL所使用的 SQL 语言是用于访问数据库的最常用标准化语言。MySQL 软件采用了双授权政策,分为社区版和商业版。因为License的问题,还有兼容MySQL的MariaDB分支可选。

    磁盘IO与预读

    因为业务上对于数据库的要求其实是比较高,主要是速度上的,经常是几毫秒的。在这种情况下硬盘IO就需要关注了。机械硬盘是需要机械运动的,比如常说的7200转。IO的消费是很大的,所以操作系统做了一些额外的优化,当执行一次IO时,不光读取当前磁盘地址的数据,也会把相邻数据都读取到内存缓冲区内,每一次IO读取的数据称之为一页,一般为4k或8k。

    所以数据库在设计上有一个重要的方向就是减少IO。

    索引

    最容易的想到的方案就是记录一些数据的IO块的位置,比如自增ID为主键,前1000条在块N,后一千条在块N+1,当然这个方案有很多弊端,所以数据库专门抽象了索引这个概念。

    索引按照官方文档的说法是帮助MySQL高效获取数据的数据结构,具体到不同的存储引擎层面,具体的数据结构是有区别的。

    数据库查询是数据库的最主要功能之一,而应用都希望查询数据的速度能尽可能的快,因此数据库系统从设计层面会对查询进行优化。而不同的算法都只能应用于特定的数据结构之上,比如二叉树查找只能应用于二叉查找树上,但是数据本身的组织结构不可能完全满足各种数据结构。

    所以,在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用或者指向的数据,同时在这些数据结构上实现高级查找算法。这种数据结构就是索引。

    具体的索引结构支持又和引擎绑定,比如最常说B树或者B+树在MyISAM,InnoDB上都是支持的。

    索引 MyISAM引擎 InnoDB引擎 Memory引擎
    B-Tree 索引 支持 支持 支持
    HASH 索引 不支持 不支持 支持
    R-Tree 索引 支持 不支持 不支持
    Full-text 索引 不支持 暂不支持 不支持

    慢查询

    慢查询就是字面的意思,查询请求运行时间比较长的就是慢查询,但是具体的时间阈值是可以变动的。比如默认的慢查询日志是10秒,而我司是100ms。

    慢查询对于应用和数据库本身是有危害的,少量的慢查询影响业务的响应,而慢查询的积累会消耗掉连接池,进而影响整个数据库的响应能力,导致大面积故障。

    MySQL是可以直接捕获慢查询,并将其记录在慢查询日志中,默认地址/var/lib/mysql/mysql-slow.log。利用show processlist也是可以监控到实时存在的慢查询。

    优化

    慢查询的优化有很多可以找到的资料,美团点评技术团队有一些总结:

    • 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整;
    • =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式;
    • 尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录;
    • 索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);
    • 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

    当然自己熟练掌握这些技巧是可以的,但是处于快速解决问题的情况下借助一些工具无疑是更好的。

    实用explain可以快速定位问题,但是需要经验才能实施优化。

    老版本的PT包含了一个pt-query-advisor工具,也可以从慢查询日志给出建议,不过后面的版本下掉了。

    美团点评开源的SQLAdvisor可以帮助分析SQL语句,同时提供优化建议。这个是非常实用的,我打了一个Docker版本的,可以快速实用。

    参考

    https://www.cnblogs.com/nixi8/p/4540813.html

    https://www.jianshu.com/p/7529a0fbf088

    https://www.jianshu.com/p/43091bfa8aa7

    https://dev.mysql.com/doc/refman/8.0/en/slow-query-log.html

  • Java中的toString

    Java中的toString

    Java中的Object包含一个toString方法,该方法默认实现是输出类名+hashcode,所以一般业务上都需要覆盖此方法实现。

    toString大部分时候都不影响功能,很大程度上是用于日志输出的时候,比如

    logger.info("user {} query keyword:{}", userId, searchParams)

    覆写该方法是必要的,我们以一个简单的类为例

    public class Hello   {
        private long id;
        private String message;
    }

    常见方法主要有以下几种:

    使用StringBuilder

    @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("Hello{");
            sb.append("id=").append(id);
            sb.append(", message='").append(message).append('\'');
            sb.append('}');
            return sb.toString();
        }

    要求最低JDK 1.5,一般的IDE都是这种

    使用StringJoiner

    如果JDK版本在1.8上,可以使用StringJoiner

    @Override
        public String toString() {
            return new java.util.StringJoiner(", ", Hello.class.getSimpleName() + "[", "]")
                    .add("id=" + id)
                    .add("message='" + message + "'")
                    .toString();
        }

    如果有第三方库,常见的guava或者apache common,那么还有一些别的方法

    使用ToStringBuilder

    @Override
        public String toString() {
            return new ToStringBuilder(this)
                    .append("id", id)
                    .append("message", message)
                    .toString();
        }

    这个是apache commons-lang中的方法,底层使用的是StringBuffer

    使用MoreObjects.toStringHelper

    @Override
        public String toString() {
            return MoreObjects.toStringHelper(this)
                    .add("id", id)
                    .add("message", message)
                    .toString();
        }

    这是guava中提供的工具,底层使用StringBuilder。

    这几种方法都需要根据类的字段变动,还有一个ReflectionToStringBuilder工具,直接使用反射来做,省去了后期变动的成本。问题就是性能和一些意外情况。