小站重新装修,欢迎到访。
分类 #技术

Omad构建期Mybatis Mapper文件语法检查

去年是忙碌的一年,今年陆续总结回顾下吧。

我目前所在的项目,使用了Mybatis做ORM,SQL是写到Mapper配置里,所以就导致一个问题,Mapper的语法无法在编译期被检查。而且Mapper错误在运行时的提醒非常晦涩,很难快速定位解决(比如resultType写成了resultMap,服务会不停的重新加载,直到OOM)。特别是开发比较紧张的时期,如果有人写了有问题的Mapper部署到开发服务器上,从发现到解决问题几个回合下来,浪费的不只是一个人的时间,被阻塞的有可能是整个研发团队。

我于是思考能否在发布前发现类似问题。翻了下资料,Mybaits没有相关检查的工具类,没办法,自己动手,丰衣足食。

细细想一下,首先是时机问题,编译期是不用考虑了,那么夹在编译期和运行时中间的只有一个构建期,我们项目用了我厂的omad发布系统,构建脚本是ant写的,想到这思路突然就有了:我在构建期启动一下看报不报错不就齐活了?

于是着手研究错误Mapper导致的异常栈,走读了相关Spring代码,发现第一个DAO方法的调用会触发所有Mapper文件的检查,那么事情就更简单了。

首先是修改构建脚本,在编译打包完成后主动调用一个main函数,启动一下Mybatis相关的部分:

1
2
3
4
<target name="check-mapper">
<echo message="Now to check mybatis mapper files"/>
<exec failonerror="true" command="java -cp ${basedir}/compressed/WEB-INF/lib/*:${basedir}/compressed/WEB-INF/classes com.netease.ysf.device.util.CompileTimeMapperChecker"/>
</target>

这里failonerror设置为true ,把这个target放到整个打包的最后一步:

1
2
3
4
5
6
7
<target name="deploy">
<echo message="begin auto deploy......"/>
<antcall target="compile"/>
<antcall target="compress-web"/>
<antcall target="cleanTarget"/>
<antcall target="check-mapper"/> <!--我在这里-->
</target>

然后最核心的问题就是这个函数该怎么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CompileTimeMapperChecker {
public static void main(String[] args) {
try {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:config/spring-for-mapper-checker.xml");
context.start();
context.getBean(AccountDAO.class).query(0L);
} catch (Exception e) {
System.out.println(e.getClass());
String msg = e.getMessage();
if (msg.contains("CannotGetJdbcConnectionException")) {
System.exit(0);
} else {
System.out.println(e.getMessage());
System.exit(-1);
}
}
}
}

如上,短小精悍,写一个Mapper检查专用的Spring配置,启动上下文后主动调用一次DAO访问,然后检查只要不是CannotGetJdbcConnectionException,就认为必然出了问题,而且极大可能是Mapper的语法不正确。

这里之所以放过CannotGetJdbcConnectionException是因为omad打包是在独立的打包机执行的,这机器必然没有我们数据库的访问权限,而且只有Mapper检查无误后才会执行真正的DAO操作,也就意味着,它能抛出CannotGetJdbcConnectionException的话,Mapper的检查就是通过的。

最后看一下专用的Spring配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="driverClassName"/>
<property name="url" value="url"/>
<property name="username" value="username"/>
<property name="password" value="password"/>
<property name="defaultAutoCommit" value="false"/>
<property name="testOnBorrow" value="true"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:config/mybatis.xml"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.netease.ysf.device.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
</beans>

注意dataSource的配置,参数的value部分随便填填就好了。

就是这么简单,我只修改了开发环境几个重要工程的打包脚本,添加了这步Mapper文件检查,其他环境也不需要浪费这个时间。后来这几段代码屡立战功,数次把错误的发布挡在运行之前。

译文:@Contended和伪共享

原文: http://robsjava.blogspot.com/2014/03/what-is-false-sharing.html

Java8引入了@Contented这个新的注解来减少伪共享(False Sharing)的发生。本文介绍了@Contented注解并解释了为什么False Sharing是如何影响性能的。

缓存行

CPU读取内存数据时并非一次只读一个字节,而是会读一段64字节长度的连续的内存块(chunks of memory),这些块我们称之为缓存行(Cache line)。

假设你有两个线程(Thread1和Thread2)都会修改同一个volatile变量x:

1
volatile long x;

如果Thread1先改变x的值,然后Thread2又去读它:

1
2
Thread 1: x=3;
Thread 2: System.out.print(x);

那么x所在缓存行上的所有64个字节的值都要被重新加载,因为CPU核心间交换数据是以缓存行为最小单位的。当然Thread1和Thread2是有可能在同一个核心上运行的,但我们此处假设两个线程在不同的核心上运行。

已知long类型占8个字节,缓存行长度为64个字节,那么一个缓存行可以保存8个long型变量,我们已经有了一个long型的x,假设x所在缓存行里还有其他7个long型变量,v1到v7:

1
x, v1, v2, v3, v4, v5 ,v6 ,v7

伪共享(False Sharing)

这个缓存行可以被许多线程访问。如果其中一个修改了v2,那么会导致Thread1和Thread2都会重新加载整个缓存行。你可能会疑惑为什么修改了v2会导致Thread1和Thread2重新加载该缓存行,毕竟只是修改了v2的值啊。虽然说这些修改逻辑上是互相独立的,但同一缓存行上的数据是统一维护的,一致性的粒度并非体现在单个元素上。这种不必要的数据共享就称之为“伪共享”(False Sharing)。

填充(Padding)

一个CPU核心在加载一个缓存行时要执行上百条指令。如果一个核心要等待另外一个核心来重新加载缓存行,那么他就必须等在那里,称之为stall(停止运转)。减少伪共享也就意味着减少了stall的发生,其中一个手段就是通过填充(Padding)数据的形式,来保证本应有可能位于同一个缓存行的两个变量,在被多线程访问时必定位于不同的缓存行。

在下面这个例子里,我们试图通过填充的方式,使得xv1位于不同的缓存行:

1
2
3
4
5
6
7
8
9
10
11
public class FalseSharingWithPadding {
public volatile long x;
public volatile long p2; // padding
public volatile long p3; // padding
public volatile long p4; // padding
public volatile long p5; // padding
public volatile long p6; // padding
public volatile long p7; // padding
public volatile long p8; // padding
public volatile long v1;
}

在你考虑使用填充之前,必须要了解的一点是JVM可能会清除无用字段或重排无用字段的位置,这样的话,可能无形中又会引入伪共享。我们也没有办法指定对象在堆内驻留的位置。

为了避免无用字段被消除,通常我们会用volatile修饰一下。个人建议只需为处于激烈竞争状态的类进行填充处理,而且一般只有通过对其性能分析才能发现性能上的不同。通常在性能分析时,最好在对其迭代访问10000次之后再去采样,这样可以消除JVM本身的运行时优化策略带来的影响。

Java8和@Contended

除了对字段进行填充之外,还有一个比较清爽的方法,那就是对需要避免陷入伪共享的字段进行注解,这个注解暗示JVM应当将字段放入不同的缓存行,这也正是JEP142的相关内容。

该JEP引入了@Contented注解。被这个注解修饰的字段应当和其他的字段驻留在不同的位置。

1
2
3
4
5
public class Point {
int x;
@Contended
int y;
}

上面的代码将x和y置于不同的缓存行。@Contented注解将y移动到远离对象头部的地方,(以避免和x一起被加载到同一个缓存行)。

参考

http://openjdk.java.net/projects/jdk8/features
http://beautynbits.blogspot.co.uk/2012/11/the-end-for-false-sharing-in-java.html
http://openjdk.java.net/jeps/142
http://mechanical-sympathy.blogspot.co.uk/2011/08/false-sharing-java-7.html
http://stackoverflow.com/questions/19892322/when-will-jvm-use-intrinsics
https://blogs.oracle.com/dave/entry/java_contented_annotation_to_help

oozie的使用总结

本篇不是oozie的教程,官网的文档虽然比较粗糙,但已经非常全面,可直接参考,下文总结下oozie使用中遇到的一些问题(持续更新中)。

oozie版本:3.3.2

###时区问题

oozie默认使用UTC时区,而服务器上可能是CST,建议统一使用GMT+0800。

  • 不要修改oozie-default.xml,无效。在oozie-site.xml中添加:
1
2
3
4
<property>
<name>oozie.processing.timezone</name>
<value>GMT+0800</value>
</property>
  • 可以用oozie info --timezones来查看支持的时区
  • 使用GMT+0800后,时间不可以再使用形如2014-01-24T13:40Z的格式,要使用对应的形如2014-01-24T13:40+0800的格式
  • 还有一点比较重要,即oozie web console的TimeZone设置要和上述一致,否则你在web console中看到的时间在感官上都是不正确的

###Hive相关
oozie会启动一个MR job来启动hive client,需要在你的oozie app里自行指定hive的配置,以及提供相关lib,因为不确定是哪一台节点,所以需要给每一台计算节点都分配hive metastore的权限。

hive-site.xml

  • 使用指定hive-site.xml位置,下例中对应位置为该oozie app所在的目录,也可以指定一个绝对的HDFS路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <action name="trackinfo">
    <hive xmlns="uri:oozie:hive-action:0.2">
    <job-tracker>${jobTracker}</job-tracker>
    <name-node>${nameNode}</name-node>
    <job-xml>hive-site.xml</job-xml>
    <configuration>
    <property>
    <name>mapred.job.queue.name</name>
    <value>${queueName}</value>
    </property>
    </configuration>
    <script>trackinfo.hql</script>
    <param>label=${wf:actionData('date')['lastday']}</param>
    </hive>
    <ok to="end"/>
    <error to="fail"/>
    </action>

####hive的lib

  • 在job.properties中设置oozie.use.system.libpath=true
  • 使用oozie.libpath=/path/to/lib指定lib路径

####metastore

  • 切记要把metastore的相应jdbc驱动放到lib里
  • 别忘了给每个计算节点授权,否则连接不上metastore

####hive action的错误如何分析?
hive action执行失败,怎么分析原因呢?在oozie的web console中,打开因出错被KILL的action节点,打开Console URL即可以看到对应的MR jobdetails页面,一般错误信息在Map的日志里,打开你就会发现,日志的内容涵盖了hive job的定义、hql以及控制台输出,足够分析错误原因了。

###前一天
由于我司大多数job是计算前一天的数据,故需在调度时动态计算前一天的日期字符串,使用shell action结合 可以捕捉控制台输出,输出格式需为A=B,这样就可以使用${wf:actionData('A')['B']}提取所需的字符串了。

1
2
3
4
5
6
7
8
9
10
11
12
13
<action name="date">
<shell xmlns="uri:oozie:shell-action:0.1">
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<exec>${cmd}</exec>
<argument>-d</argument>
<argument>1day ago</argument>
<argument>+lastday=%Y-%m-%d</argument>
<capture-output/>
</shell>
<ok to="nextStep"/>
<error to="fail"/>
</action>

其中cmd=/bin/date

###权限
遇到HDFS相关的权限问题,请通过修改oozie app提交用户或修改HDFS文件权限的方式自行解决。

1236

博主是一个不是很聪明的码农。完美主义者,强迫症中期。这里会记录一些回忆和点滴,以博为镜。

武器库:

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-SA 3.0  授权发布。最后生成于 2017-02-20.