数据库自动部署工具Flyway使用

之前做过一个数据库的工具,可以从数据库中抽取所有应用关心的信息,包括表结构、分区、索引,存储过程,函数,序列,常量参数,技术配置表的内容等。有了这些信息可以检查确保环境配置的正确性,另外也便于开发人员检索使用。近期计划在组内搭建数据库变更脚本的自动部署及管理工具,进一步提高数据库变更的效率,保证变更的正确性。目前此类开源工具有很多,我们最终决定使用Flyway,它在Java项目中很常用,它可以批量对数据库脚本进行部署,并会对当前数据库环境的部署情况进行记录跟踪。

Flyway工具使用简单,它提供了Java API、Maven插件、命令行等多种使用方式,这些具体的使用方式在此就不详细介绍了,大家可以参阅“Flyway 官方文档”。下面结合我项目的特点来介绍下Flyway的使用,也欢迎大家批评指正。

我们项目本身比较复杂,主要体现在下面几个方面:

  1. 从数据库角度整体分为联机数据库、批量数据库,对于特殊产品还会有其他数据库服务器,例如信息报告数据库、第三方支付数据库(主库、多个子库)、海外业务数据库(多个)等;
  2. 从测试环境上分为单元测试环境、PL1-PL4共4个应用组装测试环境、VT环境、生产环境,这些不同环境之间的表空间,数据库个数也有差异;
  3. 多个时间点的版本会并行进行开发测试,不同版本的测试环境也不同,且同一个测试环境的版本可能会出现跳跃。

众所周知,不同环境的数据库要尽量保持一致,这样在维护方面会简单很多,但有些差异是很难避免的。为了更好的解决不同环境的差异我们设计了一套模板体系,只需要上传一套sql变更脚本,然后通过程序来生成不同环境的特定脚本,便于部署。另外此程序还会完成变更脚本的比对工作,并输出比对结果,从而保证模板和具体环境脚本的一致性。具体程序代码见FlywayMigration Git,大家可以自由使用,后续此程序的功能还会继续完善。

目前我们的sql变更脚本的目录结构如下:

+ dbscript
|- ljdb // 联机数据库
    |- baseline //存储当前数据库的基线信息
    |- common //所有环境通用的信息
        |- 20170819 //按版本建立不同的目录
        |- 20170922
    |- template //模板信息
        |- 20170819 //按版本建立不同目录
        |- 20170922
    |- unit //单元测试
        |- xxx  //目录下内容会自动生成
    |- vt
    |- template-config.yml
|
+- pldb //批量数据库
    |-xxx //同ljdb

其中template目录下是按照版本保存的模板文件,unit、vt等特定环境目录是程序自动生成的,且不允许手工维护。 通过上面的做法可以很好的做到对环境差异的屏蔽。对于第三个版本跳跃的问题目前没想到好的方法,目前想到的方法是通过不同版本使用不同的非交叉版本号,当版本跳跃时设置Flyway允许不校验执行顺序,另外通过最开始提到数据库信息抽取工具对变更结果进行比对,从而保证正确性。

这里有个小故事想和大家分享下:最开始在安排脚本目录结构时计划将template、unit、vt等特定环境的目录放在版本点如20170819的目录下,以便同一个版本的所有变更脚本都在一起,便于维护,目录结构类似ljdb/20170822/common、unit、vt、template这样。这样一来针对某个环境部署sql脚本时就需要根据环境名称来过滤需要的脚本,例如对于unit单测环境同时需要ljdb/*/common+unit多个版本点的目录。在网上找了很多地方,发现flyway不支持这种路径过滤方式,只能把多个版本点的路径通过程序一一列举,这样体验很差,而且容易出现sql脚本部署的遗漏。官方对此的说法是通过调整目录结构来解决。当时还觉得不爽,现在回头看调整目录结构后反而觉得更清爽了。站在这个角度官方的做法反而很值得推崇,不提供不必要或者容易误用的功能。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

自动化工具Ansible简介

本网站托管在Linode下,操作系统原来是Ubuntu 14.04,近期决定将系统升级到16.04版本,为后续的软件更新做准备。经过在网上查阅相关的教程,并没有发现好的方法,比较安全的方法是重装操作系统,然后重新安装相应的软件。其实这并不是多麻烦的事,但想到以后还会有很多这样的情况,比如下次的系统升级或是网站搬家,就又要做重复的工作,就很不爽。那有没有可以自动完成这件事的方法呢?有,就是这篇博客要介绍的Ansible。

Ansible是一个自动化工具,可以很方便的完成对多个服务器的软件安装部署,也可以执行一些例行的常规任务比如备份、升级等。它是采用python开发的,但作为用户来说不需要python语言基础,只需要了解它的yaml语法、常规命令、roles的架构规范即可。

Ansible的使用方法可以参考它的官方文档,也可以看国内的中文版,可以更快上手。对于Ansible原理和最佳实践,可以参考资料1 2 3。对于Ansible的学习建议参考官方文档边学边做,并使用解决实际问题,这样会快很多,而且在解决实际问题时会体会深刻。软件安装时建议使用最新版(我使用的是2.3.1版),可以使用一些更高级的命令,简化操作。另外建议使用虚拟机或是Docker容器来练习,可以反复操作,而且不用担心玩坏了。

学习Ansible的过程除了学习具体知识外另一个大的收获是可以通过Ansible Galaxy找到很多自动化安装软件的方法,可以打开你的视野,摆脱在google上找安装教程的旧方法。

最近我在我的网站服务器上部署了NextCloud(类似Dropbox),作为自己的私人云盘,实现不同电脑文档的同步。这里把安装使用的Ansible playbook分享出来供大家参考4,详见我的github仓库nextcloud-ansible,大家可以自行下载试用。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Java agent简单介绍

近期计划做一个自动化回归测试的工具,大体实现的功能如下:

  1. 从测试环境抓取相关的日志、输入输出报文;
  2. 根据日志或报文来构造这个测试场景需要的数据,可以采用从数据库中抓取数据或从日志中解析数据;
  3. 根据输入输出报文来配置调用外部服务的挡板,挡板会负责校验输入项,并返回相应的输出;
  4. 将上述信息装配后,生成对应的测试案例。这个测试案例相当于我们应用自身的组件组装测试,可以用于回归我们自身的代码功能。

可以看出上面的测试主要为了还原测试环境的测试场景,通过使数据库、输入输出报文和测试环境完全一致来达到回归的目的。当然具体实现上还是有很多细节可以考虑的。目前考虑到的一个重要的问题就是时间的可重复性,很多测试案例都是依赖于特定的时间的,节假日、营业时间、夏令时等等,而每个测试案例自身对时间的需求又各异。这个问题是必须要解决的,其中一个方案是修改现有的代码,改为使用可测试性的时间类,例如springsideorg.springside.modules.utils.time.ClockUtil类;另一个方案是本文要介绍的,使用java agent的方式对应用的字节码进行修改,使其中的时间具有可测试性,这种方案的好处是无需修改应用代码,做到应用透明。

具体java agent的详细使用可以参考1 2,这种技术在很多优秀工具中都有使用,比如JmockitJaCoCoJRebel等场景下都有使用。

具体实现代码包括如下几部分: ## 1.代理类的main类

//file-name:FtmAgent.java
package com.zyh.ftmagent;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class FtmAgent {
    public static void premain(String agentArgs, Instrumentation inst)
            throws ClassNotFoundException, UnmodifiableClassException{
            inst.addTransformer(new TimeTransformer());
        }
}

2.字节码转换类

这里的处理分为两部分,一个是找到需要加工的类的方法;另一个是对这些方法进行修改,将其中的调用一般方法和new形式的字节码进行修改。字节码修改部分采用Javassist3来编写,它相对于其他工具如ASM更加易用,相关教程可以参考Javassist Tutorial

//file-name:TimeTransformer.java
package com.zyh.ftmagent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.NotFoundException;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import javassist.expr.NewExpr;

public class TimeTransformer implements ClassFileTransformer {

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 仅对于com.zyh.test包下的类进行替换
        if (className.startsWith("com/zyh/test")) {
            byte[] transformed = null;
            System.out.println("Transforming " + className);
            ClassPool pool = ClassPool.getDefault();
            CtClass cl = null;
            try {
                cl = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
                if (cl.isInterface() == false) {
                    CtBehavior[] methods = cl.getDeclaredBehaviors();
                    for (int i = 0; i < methods.length; i++) {
                        if (methods[i].isEmpty() == false) {
                            doMethod(methods[i]);
                        }
                    }
                    transformed = cl.toBytecode();
                }
            } catch (Exception e) {
                System.err.println("Could not instrument    " + className + ",    exception : " + e.getMessage());
            } finally {
                if (cl != null) {
                    cl.detach();
                }
            }
            return transformed;
        }
        return null;
    }

    private void doMethod(CtBehavior method) throws NotFoundException, CannotCompileException {
        // 修改方法体的定义
        method.instrument(new ExprEditor() {

            // 修改一般的方法调用语句
            public void edit(MethodCall m) throws CannotCompileException {
                try {
                    if (m.getClassName().equals("java.lang.System")) {
                        if (m.getMethodName().equals("nanoTime")) {
                            m.replace("{long rslt = com.zyh.ftmagent.MySystemTime#nanoTime(); $_ = ($r)rslt; }");
                        } else if (m.getMethodName().equals("currentTimeMillis")) {
                            m.replace(
                                    "{long rslt = com.zyh.ftmagent.MySystemTime#currentTimeMillis(); $_ = ($r)rslt; }");
                        }
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                    throw new RuntimeException(ex);
                }
            }

            // 修改new Date()这种格式的代码,替换其中的class类
            public void edit(NewExpr e) throws CannotCompileException {
                try {
                    if (e.getClassName().equals("java.util.Date")) {
                        e.replace("{$_ = new com.zyh.ftmagent.MyDate();}");
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                    throw new RuntimeException(ex);
                }
            }
        });
    }
}

3.自定义的日期类

包括两个类,具体如下:

  • MyDate:对默认Date类进行了扩展,实现根据MysystemTime获取的当前时间进行构造Date类。
  • MySystemTime: 实现了currentTimeMillisnanoTime方法,会根据最初设置的当前时间间隔进行设置。为了保证线程安全,这里使用了ThreadLocal类,可以在程序中的公共部分调用setTimeDiff方法对时间进行重新定义。
//file-name: MyDate.java
package com.zyh.ftmagent;

import java.util.Date;

public class MyDate extends Date {
    private static final long serialVersionUID = 1L;

    public MyDate(){
        super(MySystemTime.currentTimeMillis());
    }
}

//file-name:MySystemTime.java
package com.zyh.ftmagent;

public class MySystemTime {
    //默认为当前时间
    private static final Long INIT_VALUE = 0l;

    private static ThreadLocal<Long> timeDiff = new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            return INIT_VALUE;
        }
    };

    public static long currentTimeMillis() {
        return System.currentTimeMillis() - timeDiff.get();
    }

    public static long nanoTime() {
        return System.nanoTime() - timeDiff.get() * 1000;
    }

    /**
     * 设置距离当前的时间,为正表示后退多少ms
     * 
     * @param diff
     */
    public static void setTimeDiff(long diff) {
        timeDiff.set(diff);
    }
}

4.MANIFEST.MF文件

其中的Premain-ClassFtmAgent的主类,在代码编译好后需要把上述的所有代码打成jar包(例如包名为ftm-agent.jar),以备使用。

Manifest-Version: 1.0
Premain-Class: com.zyh.ftmagent.FtmAgent
Boot-Class-Path: javassist-3.22.0-CR1.jar

测试及其他

测试类需要在com.zyh.test包下,测试前需要执行MySystemTime.setTimeDiff来将时钟进行回拨。如在eclipse中测试需要在vm的参数中增加 -javaagent=ftm-agent.jar,然后执行测试的main函数即可。完整的代码见FtmAgent-Gist

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

论管理能力与技术能力

近期一直在准备项目管理的考试,现在算是告一段落,这里把自己的一些想法整理记录一下,希望也能给大家有所帮助。

这里主要说一下管理和技术的不同。其实两者差别还是挺大的,甚至很多题目都是以资深技术人员担任项目经理遇到的问题来出题的(看的我都有些深有同感,哈哈)。我个人算是比较偏技术的了,对技术也有些研究,喜欢学习一些新东西,也喜欢做一些有挑战的事。下面从我自身的体会分别说下我对技术和管理的看法:

技术更偏向于自身能力的提高,这体现在深度(例如某方面的专家)和广度(例如对整个架构的理解),但不管如何都是侧重自身见解、能力。这些能力往往是可以看出来的,可以从你对于某个问题的看法,甚至从你对于某个问题的解决方法,从你认为事情的难易程度以及问题的关注点上都会有所体现。这就是所谓的高度(或者说格局),这对于技术人员的成长很重要。作为技术人员你需要不断学习新知识,另外也需要对底层知识例如操作系统、算法等有所有了解,在内心深处搭建属于你自己的知识大厦。学习这些知识是需要花费大量的时间的,包括阅读和练习。长期的与机器或书本打交道,也导致很多技术人员不善于与人交流,不善言辞。

管理能力则更侧重于领导力,能够带领团队一起完成一件事情,充分利用每个人的特长,各司其职,共同向着某个目标迈进。一个合格的领导不但要有对本行业很了解,更需要有强大的领导力,很好的沟通协调能力,风险控制能力。通过学习项目管理的知识,也让我对项目管理有了系统的了解。项目管理从整体上看,包括项目整体管理、立项管理、范围管理、时间管理、成本管理、质量管理、人力资源管理、沟通管理、风险管理、采购管理、配置管理。作为一个合格的项目经理,需要制定统一的项目计划,明确项目范围,并做好进度控制、风险控制、成本控制、质量控制,并且要做好团队建设、建立一个综合素质过硬的团队,通过合理的激励手段使项目组成员有成就感。可以看出这里面更强调的是沟通协调、问题识别跟踪、合理的计划安排、合理的制度流程。所以很多从技术转过来的人很容易不太适应角色转换,从而虽然自己技术很强,但项目整体进度滞后、客户不满意、需求蔓延、质量低下。最近我看一些电视或电影时也重点关注了里面的领导角色,发现好的角色都是有担当的,能够心系团队,他们有独特的人格魅力,在危险的时刻冷静应对,带领团队为了目标努力奋斗,另外合格的领导最重要的一点就是要有眼光,为团队指明方向。

上面说的并不是做技术的人就不能当领导,相反做技术的当领导是有自己的优势的。这种优势首先体现在自己的专家权力,能够对问题有准确的判断,如果再加上对业务的了解,就可以有更高的眼界,更大的格局。做领导的人尤其是一线领导,如果对技术一点不懂是很被动的,往往也不能服众,当手下遇到问题你也只能给予心理上的安慰了。我更倾向于认为技术领导是一种知识跨界,通过对管理领域的了解来使你的综合能力得到提升,最大化发挥你的价值(不仅仅是你个人的价值,而是整个团队的价值)。当一个领导思考问题的时候开始以团队的角度思考,也就是他开始转变为一个领导者了。

我个人接触管理有段时间了,通过项目管理的学习,让我开始对管理有了系统的理解。很多事情我也开始结合自己的工作的状态进行思考,很多事情都变得豁然开朗了,也更加明确遇到问题应该如何解决,如何正确的做正确的事情。管理是一门很大的学问,我后面也会努力提升自己,包括理论和实践,让我的团队成员跟着我提升自我、快速成长。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

账务系统热点帐号问题

账务系统的热点帐号是一个很常见的问题,特别是随着互联网支付的发展,热点帐号的问题更加突显。很多支付机构由于业务限制往往在一个银行只有一个对公帐号,这一个帐号往往就是热点帐号。

对于不同帐号的并发处理相对比较简单,比如按客户或帐号拆表、拆库,这之间比较复杂的是分布式事务问题。目前处理分布式事务一般的解决方案包括:

  1. 可靠消息+补偿模式;
  2. TCC分布式事务模式;
  3. 两阶段提交;
  4. 拆分为多个局部事务。

这些方案往往通过自动任务来处理异常的场景从而保证最终一致性。

如果说去IOE最困难的问题应该就数热点帐号问题了(或者说分布式事务问题),因为单个帐号的扣减余额等肯定要通过事务来保护,防止出现余额和流水不符的情况。但事务必然会导致并发度下降,即使对于功能很强大的主机核心系统,并发度也会显得捉襟见肘。为了防止自己处理能力超限,主机往往会进行流控以保护自己,但这样的用户体验会很差。如果这件事在开放系统中做,由于每个服务器性能有限,并发度会更低。下面介绍下自己对热点帐号问题的理解,因我本人并不负责核心系统,有不当之处欢迎指正。大体解决方案分为如下几种:

1. 汇总记账方式

汇总记账分为两种,收单和付款。其中收单采用异步入账的方式比较好,可以将钱先放在内部过渡户中(过渡户可以是一个或多个),然后异步将钱整批汇总后入收方帐号。对于付款会复杂些,主要涉及到透支的问题,如果业务上允许透支也可以采用先银行垫付一段时间,然后整批一笔入付方帐号;如果业务不允许透支就比较复杂了,一种可行的方案是将每笔账务先预先记录流水然后返回“处理中”,后台采用批量的形式以批量为单位更新付方帐号的余额。但这样带来的一个问题是响应不够及时。

2. 异步缓冲记账

参考资料1中提到的削峰填谷方式,思路比较简单,对于不超过并发数的交易同步响应,对于超过并发的交易放在缓存队列慢慢处理,可以返回处理中或成功(返回成功有透支风险)。这种方式本质并没有提高系统的并发量,只能算是一种简单体验改进。

3. 同步批量缓冲记账

在参考资料2中提到了一种可行的解决方式,但并没有介绍如何实现,结合参考资料3,可行的解决方案如下。当接到客户的转账请求后,如果为热点帐号,则在执行完所有的校验后,将交易放在一个队列(或多个队列)中。针对每个队列会有单一的线程进行处理,它一次或获取多条待处理记录然后统一记录数据库流水,对于热点帐号的余额扣减只扣减一次。对于这种方法如何确定队列的大小以及同步等待的时间是一个需要分析的问题。这种方法可以成倍减少数据库的压力,类似与数据库事务管理器的提交机制,但它也带来一个问题就是增加了单笔交易的延时。为了使这种方法收益最大化,在框架设计层面需要把同一个热点帐号的交易转到特定的服务器来处理。

4. 建立多个影子帐号

上述“同步批量缓冲入账”方式的限制是单台应用服务器的处理上限,单台服务器的处理上限。为了解决此问题,一种可行的解决方案是将一个热点帐号拆分为多个影子帐号,每个影子帐号有自己的余额和流水信息。所有影子帐号的余额之和是整个帐号的余额。拆分后的处理逻辑和单个帐号的处理方式相同,可以采用不同的处理方式,如异步缓冲记账或是同步批量缓冲记账等。这个方案需要特殊考虑的点包括:

  1. 帐号余额增加时如何分配给其他的影子帐号:为避免分布式事务,建议采用异步方式处理。当然由于这个交易量很少,采用分布式事务也是可行的。
  2. 当单个影子帐号余额不足但整体帐号余额充足时的处理方式:可以采用“直接报错“”或是“返回处理中”改为异步处理。对于客户的特殊提取交易不应直接报错,因为报错则无法支持客户转出所有余额。对于异步处理的方案在返回处理中时可将此笔账务交易转移到其他影子帐号处理,如果所有影子帐号均余额不足但整体余额充足时,此时需要进行影子帐号间调账,将此笔交易放入特殊队列,将此影子帐号置为停用,在处理时将此影子帐号的钱转给其他影子帐号然后继续处理。直到所有的帐号均不支持则进行报错。可以看出当开始停用影子帐号时系统整体的并发度会显著降低,所以采用影子账号应让客户知晓,并提前做好客户的工作,例如帐号至少要保留特定余额、指定特殊的销户流程、与客户约定好各种异常的处理策略等等。
  3. 多个影子帐号之间需要保证负载均衡问题:另外需支持动态增加和减少影子帐号的个数。

另外提一下,内存数据库并不是一个好的解决方案,首先如何保证内存数据库和关系数据库的数据一致性是一个困难的问题;另一方面内存数据库本身也是一个单点,会存在单点问题;再一方面关系数据库本身对于热点数据也会做内存级别缓存,所以内存数据库意义并不是很大。

上面介绍了很多方案,可以看出并没有完美的方案,热点帐号方案更多的考虑是一种权衡,我们需要根据不同的情况来指定不同的策略。后续我会试着对于方案三、四进行实现,看是否有其他问题。后面如发现更好的解决方案也会更新此文章,也欢迎大家告知其他解决方案。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》