spring quartz 指定机器运行trigger

我们在使用spring-quartz的时候,常常会遇到一个场景,那就是任务的调度会随着争取锁的先后顺序而出现不固定机器执行的场景,这在正常业务逻辑中具备了很好的容灾能力,但是在我们排查问题时,却带来了困绕,如果出现问题,我们期望任务调度固定在一台机器上进行执行,方便我们对问题的定位和排查。

这里就探讨如何扩展spring-quartz来实现任务的固定机器执行。

整体思路

要指定机器运行trigger,那首先我们必须清楚spring-quartz cluster模式下,任务的触发时如何进行分布式执行的。
spring-quartz cluster 是借助数据锁来实现并发控制的,需要注意的是分布式环境下需要保证各机器系统时间一致性;

核心处理线程QuartzSchedulerThread决定了扫描那些JOB,以及触发执行和生命周期的维护。
QuartzSchedulerThread中run方法:

//在trigger表中扫描指定SCHED_NAME、状态为WAITING,下次触发时间在30秒内的触发器
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
 now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
.... 
//触发器执行
List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers); 
...
//释放触发器
qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
...
//完成触发器
qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);

我们就可以在acquireNextTriggers中做扩展,获取全部或者指定了实例ID的trigger。

延伸思考:acquireNextTriggers获取到trigger列表后,假设机器宕机,这些trigger如何路由到其他机器中正常运行?

 

扩展实例ID的生成策略

spring-quartz一般配置的org.quartz.scheduler.instanceId:AUTO,采用的是SimpleInstanceIdGenerator ID生成策略。

public class SimpleInstanceIdGenerator implements InstanceIdGenerator {
 public String generateInstanceId() throws SchedulerException {
 try {
 return InetAddress.getLocalHost().getHostName() + System.currentTimeMillis();
 } catch (Exception e) {
 throw new SchedulerException("Couldn't get host name!", e);
 }
 }
}

SimpleInstanceIdGenerator 采用机器主机名称与当前时间戳作为instanceId,我们期望在开发环境使用hostName,在生产环境(Linux)采用IP作为实例ID。

实现InstanceIdGenerator接口,实现自己的ID生成策略,QuartzSchedulerInstanceIdGenerator的实现逻辑为:

public class QuartzSchedulerInstanceIdGenerator implements InstanceIdGenerator {

private static final Logger log = LoggerFactory.getLogger(QuartzSchedulerInstanceIdGenerator.class);

private static final String OS_NAME = "os.name";

private static final String WINDOWS = "Windows";

private static final String MAC = "Mac OS";

@Override
 public String generateInstanceId() throws SchedulerException {
 String id;
 try {
 if (isLocalDev()) {
 id = getHostName();
 } else {
 id = IpUtil.getIp();
 }

if (StringUtils.isBlank(id)) {
 id = InetAddress.getLocalHost().getHostName() + System.currentTimeMillis();
 }
 } catch (Exception e) {
 throw new SchedulerException("Couldn't generate instance id!", e);
 }

return id;
 }

private String getHostName() throws SchedulerException {
 try {
 return InetAddress.getLocalHost().getHostName();
 } catch (Exception e) {
 throw new SchedulerException("Couldn't get host name!", e);
 }
 }

private boolean isLocalDev() {
 if (StringUtils.indexOfIgnoreCase(System.getProperty(OS_NAME), WINDOWS) >= 0) {
 return true;
 }

if (StringUtils.indexOfIgnoreCase(System.getProperty(OS_NAME), MAC) >= 0) {
 return true;
 }

return false;
 }
}

在quartz.properties中配置:

org.quartz.scheduler.instanceIdGenerator.class=com.xx.ext.QuartzSchedulerInstanceIdGenerator

扩展StdJDBCDelegate

SchedulerFactoryBean 工厂Bean负责加载配置信息,初始化SchedulerFactory和Scheduler实例,jobStore负责job、trigger等的持久化工作,针对不同的数据库类型,可以配置不同的DriverDelegate。Spring中默认使用LocalDataSourceJobStore作为JobStore的处理类。

SchedulerFactory中initSchedulerFactory方法:

 if (this.configLocation != null) { 
   if (this.configLocation != null) { 
     if (logger.isInfoEnabled()) { 
       logger.info("Loading Quartz config from [" + this.configLocation + "]"); 
     } 
     PropertiesLoaderUtils.fillProperties(mergedProps, this.configLocation);  
 }
     CollectionUtils.mergePropertiesIntoMap(this.quartzProperties, mergedProps);
    if (this.dataSource != null) { 
      mergedProps.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName());
    }

可以看到SchedulerFactoryBean中设置PROP_JOB_STORE_CLASS属性是在合并用户设置的配置文件之后,也就是PROP_JOB_STORE_CLASS的实现类Spring强制指定为LocalDataSourceJobStore而无法更改,即便是我们在quartz.properties中配置了org.quartz.jobStore.class属性,也会被LocalDataSourceJobStore覆盖掉。

LocalDataSourceJobStore继承自JobStoreSupport,JobStoreSupport默认配置使用StdJDBCDelegate作为与数据库交互的代理处理类;我们可以通过扩展StdJDBCDelegate类,来实现底层数据库交互的扩展。通过配置org.quartz.jobStore.driverDelegateClass属性,指定driverDelegate为我们扩展后的Delegate。

在quartz.properties中配置:

org.quartz.jobStore.driverDelegateClass=com.xx.ext.StdJDBCDelegateExt

针对${table_prefix}_quartz_triggers表新增字段INSTANCE_NAME,此字段为InstanceIdGenerator生成的实例ID,此字段会透传到StdJDBCDelegate中的属性instanceId中,因此我们可以扩展StdJDBCDelegate,通过instanceId字段来指定获取的trigger列表。

public class StdJDBCDelegateExt extends StdJDBCDelegate {

String INSTANCE_NAME_VALUE = "{2}";

String SELECT_NEXT_TRIGGER_TO_ACQUIRE_EXT = "SELECT "
 + COL_TRIGGER_NAME + ", " + COL_TRIGGER_GROUP + ", "
 + COL_NEXT_FIRE_TIME + ", " + COL_PRIORITY + " FROM "
 + TABLE_PREFIX_SUBST + TABLE_TRIGGERS + " WHERE "
 + COL_SCHEDULER_NAME + " = " + SCHED_NAME_SUBST
 + " AND " + COL_TRIGGER_STATE + " = ? AND " + COL_NEXT_FIRE_TIME + " <= ? "
 + " AND (" + COL_INSTANCE_NAME + " IS NULL OR " + COL_INSTANCE_NAME + " = " + INSTANCE_NAME_VALUE + ")"
 + "AND (" + COL_MISFIRE_INSTRUCTION + " = -1 OR (" + COL_MISFIRE_INSTRUCTION + " != -1 AND "
 + COL_NEXT_FIRE_TIME + " >= ?)) "
 + "ORDER BY " + COL_NEXT_FIRE_TIME + " ASC, " + COL_PRIORITY + " DESC";

/**
 * 允许设置trigger 指定一台机器进行任务调度执行;
 *
 * @param conn
 * @param noLaterThan
 * @param noEarlierThan
 * @param maxCount
 * @return
 * @throws SQLException
 */
 @Override
 public List<TriggerKey> selectTriggerToAcquire(Connection conn, long noLaterThan, long noEarlierThan, int maxCount)
 throws SQLException {
 PreparedStatement ps = null;
 ResultSet rs = null;
 List<TriggerKey> nextTriggers = new LinkedList<TriggerKey>();
 try {
 ps = conn.prepareStatement(rtp(SELECT_NEXT_TRIGGER_TO_ACQUIRE_EXT, instanceId));

// Set max rows to retrieve
 if (maxCount < 1) {
 maxCount = 1;
 }
 ps.setMaxRows(maxCount);

// Try to give jdbc driver a hint to hopefully not pull over more than the few rows we actually need.
 // Note: in some jdbc drivers, such as MySQL, you must set maxRows before fetchSize, or you get exception!
 ps.setFetchSize(maxCount);

ps.setString(1, STATE_WAITING);
 ps.setBigDecimal(2, new BigDecimal(String.valueOf(noLaterThan)));
 ps.setBigDecimal(3, new BigDecimal(String.valueOf(noEarlierThan)));
 rs = ps.executeQuery();

while (rs.next() && nextTriggers.size() <= maxCount) {
 nextTriggers.add(triggerKey(
 rs.getString(COL_TRIGGER_NAME),
 rs.getString(COL_TRIGGER_GROUP)));
 }

return nextTriggers;
 } finally {
 closeResultSet(rs);
 closeStatement(ps);
 }
 }

protected final String rtp(String query, String instanceName) {
 return MessageFormat.format(query,
 new Object[] {tablePrefix, getSchedulerNameLiteral(), "'" + instanceName + "'"});
 }
}

至此,完成了指定trigger在特定instanceId上运行,但是有一个问题,假如我们期望trigger可以在多台instanceId上随机执行的话,该如何实现呢?

 

 

 

 

 

 

Spring中property-placeholder的使用与解析

在我们程序开发中,进程会需要把一些变量通过property方式进行提取,方便不同环境配置不同的属性,替换变量的方法通常有两种,一种是静态替换,一种是动态替换;所谓静态替换,是在打包编译的时候,把变量替换掉,动态替换,是在程序运行起来时,通过把属性注入到程序的环境变量中,类初始化的时候,再使用环境变量进行替换的一种方法。

静态替换常用工具:autoconfig
动态替换常用工具:spring.property-placeholder

spring动态替换变量实践

简洁配置法:

<context:property-placeholder location="classpath:xxx.properties" 
ignore-unresolvable="true"/>

等价于:

<bean id="propertyConfigurer" class=
"org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
 <property name="location">
 <value>myConfig.properties</value>
 </property>
 </bean>

完整配置属性:

<context:property-placeholder 
 location="" 
 file-encoding="" 
 ignore-resource-not-found="" 
 ignore-unresolvable="" 
 properties-ref="" 
 local-override="" 
 system-properties-mode="" 
 order="" 
/>

ignore-resource-not-found:如果属性文件找不到,是否忽略,默认false,即不忽略,找不到将抛出异常

ignore-unresolvable:是否忽略解析不到的属性,如果不忽略,找不到将抛出异常

order:当配置多个<context:property-placeholder/>时的查找顺序

不推荐将ignore-resource-not-found和ignore-unresolvable的值设置为ture,默认为false,可以有效避免程序运行异常。

使用PropertySource注解配置

Spring3.1添加了@PropertySource注解,方便添加property文件到环境.

properties的注入与使用

java中使用@Value注解获取:

@Value( "${jdbc.url}" )
private String jdbcUrl;

在Spring的xml配置文件中获取:

<bean id="dataSource">
 <property name="url" value="${jdbc.url}" />
</bean>

配置多个property-placeholder属性

Spring容器是采用反射扫描的发现机制,通过标签的命名空间实例化实例,当Spring探测到容器中有一个org.springframework.beans.factory.config.PropertyPlaceholderCVonfigurer的Bean就会停止对剩余PropertyPlaceholderConfigurer的扫描,即只能存在一个实例!

所以一遍不建议配置多个property-placeholder对象,但是在必须使用多个的场景下,如何配置呢?

<context:property-placeholder location="xxx.properties" ignore-unresolvable="true"/>
<context:property-placeholder location="xxx.properties" ignore-unresolvable="true"/>

需要设置ignore-unresolvable=”true”,否则后面的property-placeholder不会被加载;ignore-unresolvable单独使用来看是“是否忽视不存在的配置项”,不仅如此,其还有一个隐含意思:是否还要扫描其他配置项:如果为false,则会忽视后续的property-placeholder,如果需要配置多个property-placeholder则应该设置为true;

context:property-placeholder 工作原理

在 ContextNamespaceHandler 中对于 context中的property-placeholder 标签,会采用PropertyPlaceholderBeanDefinitionParser解析器进行解析;

public class ContextNamespaceHandler extends NamespaceHandlerSupport {
 @Override public void init() { 
    registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser()); 
    registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser()); 
    registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser()); 
    registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser()); 
    registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser()); 
    registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); 
    registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser()); 
    registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser()); 
  }
}

PropertyPlaceholderBeanDefinitionParser解析器会将property-placeholder 标签解析为一个PropertySourcesPlaceholderConfigurer的单例 bean 。

可以看出 PropertySourcesPlaceholderConfigurer 或者 PropertyPlaceholderConfigurer 仅仅是做了一个配置文件的解析工作,真正的注入并不由它们完成,而是托付给了Spring 的Bean初始化流程。
之所以这么做可以生效,是因为这两个类实现了 BeanFactoryPostProcessor 接口,这个接口的优先级高于后续的Spring Bean。

属性元素的注入依赖于 AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues。 通过解析PropertySourcesPlaceholderConfigurer 查询得到元素值。

public PropertyValues postProcessPropertyValues(PropertyValues pvs, 
PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
 InjectionMetadata metadata = this.findAutowiringMetadata(beanName, bean.getClass());
    try {
        metadata.inject(bean, beanName, pvs);
        return pvs;
    } catch (Throwable var7) {
        throw new BeanCreationException(beanName,
        "Injection of autowired dependencies failed", var7);
    }
 }

PropertySourcesPlaceholderConfigurer本质上是一个BeanFactoryPostProcessor。解析XML的流程在BeanFactoryPostProcessor之前, 优先将配置文件的路径以及名字通过Setter传入PropertySourcesPlaceholderConfigurer。

如上BeanFactoryPostProcessor的优先级又优于其余的Bean。因此可以实现在bean初始化之前的注入。

Spring @Value注入流程

  1. Spring Context 的初始化开始
  2. 读取到context:property-placeholder标签或者PropertySourcesPlaceholderConfigurer
  3. 解析并实例化一个PropertySourcesPlaceholderConfigurer。同时向其中注入配置文件路径、名称
  4. PropertySourcesPlaceholderConfigurer自身生成多个StringValueResolver备用,Bean准备完毕
  5. Spring在初始化非BeanFactoryPostProcessor的Bean的时候,AutowiredAnnotationBeanPostProcessor 负责找到Bean内有@Value注解的Field或者Method
  6. 通过PropertySourcesPlaceholderConfigurer寻找合适的StringValueResolver并解析得到val值。注入给@Value的Field或Method。(Method优先)2
  7. Spring的其他流程。

 

参考:

http://blog.csdn.net/qyp199312/article/details/54313784

Molly 安装与测试

Molly下载地址:https://github.com/palvaro/molly

首先阅读tutorial.md文件,安装方法按照Installation.md来执行;

需要注意点:

  1. 在mac OS下不能使用brew install来安装apr、apr-util,需要下载这两个组件的源码,通过make、install的方式进行安装;
  2. bin 目录下只有c4,缺少z3的安装文件,需要自己下载安装;
  3. 依赖安装完毕后,需要将依赖的库添加到PATH中;

安装Scala构建工具:sbt

brew install sbt@1

参考:http://www.scala-sbt.org/download.html

继续阅读

如何提高微服务架构的可用性

业界通常用多少个9来衡量系统的可用性,如99.99%表示一年中有1小时左右的不可用时间。任何一个服务的可用性都不会是100%,意味着在服务运行时间里还是有可能发生故障。当把功能集中且运行在同一个应用中的单体架构拆分成多个相互独立的微服务架构后,虽然可以降低一损俱损的全局性故障风险,但由于微服务之间存在大量的依赖关系, 随着微服务个数的增多,依赖关系也将会变得越来越复杂,而且每个微服务都有可能发生故障,如果不能做好相互依赖的隔离,避免故障的连锁反应,结果可能比单体更糟糕。

假设有100个微服务,并且每个微服务只会发生1种故障,那么总共会有 2100 种不同的故障场景,而每个微服务自身可能不止1种故障。当某个微服务发生故障时,如何确保不会导致其他依赖的微服务不可用, 如何确保系统自动降级把发生故障的微服务排除出去,如何确保故障不会扩展到整个系统? 那么如何有效确保微服务架构的可用性将会成为挑战。 继续阅读

Blue-green Deployments, A/B Testing, and Canary Releases

A lot of teams I talk to recently are very interested in “DevOps” (whatever that means… seems to mean different things to different people?) and when we sit down and talk about what that really means, the direction of the conversation can go down many interesting paths. And some times, the path it goes down makes people feel very uncomfortable. I was talking with a team a while back about deployment best practices, hot deployments, rollbacks etc and when I mentioned blue-green deployments, they became a bit queasy. Another team couldn’t understand why doing something they’ve always done was not such a very good thing.

blue-green deployments have been practiced at places like Amazon for 10+ years. They’re a safe, proven, method. Now, blue-green deployments are not a silver bullet, but there’s an element of usefulness to them. But what about A/B testing then? Or even Canary testing? With all of the #microservices, DevOps, and cloud-native talk, there’s a lot of discussion about them, but I wanted to clarify their differences. 继续阅读

The Twelve-Factor App

简介

如今,软件通常会作为一种服务来交付,它们被称为网络应用程序,或软件即服务(SaaS)。12-Factor 为构建如下的 SaaS 应用提供了方法论:

  • 使用标准化流程自动配置,从而使新的开发者花费最少的学习成本加入这个项目。
  • 和操作系统之间尽可能的划清界限,在各个系统中提供最大的可移植性。
  • 适合部署在现代的云计算平台,从而在服务器和系统管理方面节省资源。
  • 将开发环境和生产环境的差异降至最低,并使用持续交付实施敏捷开发。
  • 可以在工具、架构和开发流程不发生明显变化的前提下实现扩展。

这套理论适用于任意语言和后端服务(数据库、消息队列、缓存等)开发的应用程序。

继续阅读

DevOps的三种方式

前言

这篇博客中提到的“三种方式“源自 《DevOps Handbook》 及《凤凰项目》(The Phoenix Project: A Novel About IT, DevOps, and Helping Your Business Win.),这三种方式描述了构成 DevOps 的理论框架、流程、实践及价值观和哲学。

感谢《Lean IT》的作者 Mike Orzen 为此文提供宝贵建议。

三种方式

下文将介绍三种模式及在该种模式指导下的 DevOps 实践。

第一种方式: 系统思考

第一种方式强调全局优化,而非局部改进。— 大到部门职能划分(例如研发部和运维部门),小到个人(开发和系统工程师)。

这种方式将关注点放在整个业务价值流上。换句话说,整个团队应该关注在从需求被定义到开发,再到运维这个过程,直到价值被以服务的形式交付给最终用户。

将这种方式带到实践中的产出便是永远不要将已知的缺陷传递到下游工作,永远不要为了局部优化影响了整体价值流交付,总是为了增加价值流动努力,永远追求对架构的深刻理解。

涉及到这种方式的实践有:

  • 所有环境和代码使用同一个仓库,将软件包纳入版本管理
  • 团队共同决定发布流程
  • 保持 DEV、TEST、PRODUCTION 环境的一致性
  • 自动化回归测试
  • 小步提交,每日部署;而不是一次部署大量变更
  • 更快、更频繁发布

第二种方式:经过放大的反馈回路

第二种方式是创建从开发过程下游至上游的反馈环。几乎所有的流程改进都是为了从时间上缩短和从覆盖面上放大反馈循环,从而可以不断地进行必要的改正。

第二种方式的产出是关注到价值流中所有涉及到的用户,包括价值流内部和外部的,缩短和放大反馈回路,并且可以随时定位到需要改进的地方。

涉及到这种方式的实践有:

  • 代码审查及配置变更检查
  • 有纪律的自动化测试,使许多同时的小型敏捷团队能够有效地工作
  • 尽早设置监控预警
  • 修复 bug 为团队最高优先级
  • 团队成员之间高度互相信任
  • 团队之间保持沟通和良好合作

第三种方式:持续做试验和学习的文化

第三种方式提倡持续做试验,承担风险、从失败中学习;通过反复实践来达到精通

我们需要实验和冒着失败的风险,及时不断地尝试将我们置于一个危险的境地,我们要通过反复试错来掌握使我们远离危险的技能。

第三种方式的输出为为改善日常工作分配时间、奖励团队冒险精神,将错误人工引入系统以提高系统健壮性。

最具有代表性的就是 Netfilx 的 Chaos monkey ,Netflix 在他们的生产环境搭建一个服务用于定时随机关闭服务器,用以模拟服务器正常损坏或服务异常,他们的系统长期在这种环境下运行,“服务器故障”成为系统每日都要面临的问题,因此当服务器真的以外故障时不会对系统整体造成任何的影响。

译者后记

全局优化、快速反馈、鼓励失败。我们发现其实敏捷、精益、持续交付、DevOps中间有很多相似的东西。

参考资料: https://es.slideshare.net/SonatypeCorp/devops-connect-josh-corman-and-gene-kim-discuss-devopssec

http://itrevolution.com/the-three-ways-principles-underpinning-devops/

转发自:@duyidong

《那时花开》

作品:《那时花开》   字数:约 35000 字      体裁:中篇小说    关键字:校园青春

背景:小说记录了自己以在高中时期一群小伙伴的学习生活,作者:陈晓雷

袁河是高二上学期转到陆可凡他们班的。至于转班的原因却不得而知。不过众所周知的是,陆可凡他们班的语文老师是袁河的爸爸,所以就有人猜测这可能是他爸爸的注意,目的很简单,无非就是为了方便监督和管教儿子学习。那为什么到了高二才转班呢?答案也很简单,因为高二是承上启下的关键学期,既能延续高一时的学习状态,又能为高三做准备,所以这时转班最合适不过了。

转到陆可凡他们班上后,袁河发现了一个奇怪的现象。

比如一般课间休息的时候,同学们都会以固有的方式来放松自己。其中少数同学会走出教室,站在外面的走廊上,抬头仰望天空,认真盘算自己忧伤的心事。或者靠在旁边的栏杆上,低头看着地面,仔细回忆某天的快乐心情。而大多数同学则会三五成群地聚在一起,找一个大家都感兴趣的话题,然后各抒己见,开始谈论。 继续阅读

Taobao SSO 跨域登录过程解析

今年的双十一和双十二已经告一段落,你是否买到了你想要的宝贝呢?我们知道双十一是天猫的主场,双十二是淘宝的主场,你有没有注意到你在登录了淘宝后,访问天猫或者飞猪,你还是处于登录态的,但是我们知道cookie是不能跨域的,那么阿里是如何做到了多域名下的登录态同步呢?接下来我们通过抓包进行请求解析来了解这个过程。

基础知识:

  1. 如果忘了Cookie和Session的区别,那么建议你先回顾一下,可以参考:https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/06.1.md
  2. 如果不知道为什么需要鉴权,为什么需要SSO,为什么需要跨域登录,建议你先阅读上一篇文章“系统权限控制”。

继续阅读