Fork me on GitHub

ActiveMQ消息消费慢问题排查

Published on:

问题现象

有的时候会发现ActiveMQ中某个个队列的消息在写入后,不是立刻就被调度消费,而是需要等待一小会才能被调度消费(大概时间是1分钟),而且还伴随着这样的现象,当消息写入速度很快时消费很快,当消息写入消息速度很慢时反而消费很慢,我们的理解就是当写入慢的时候很多消费者都是闲置的那为什么消费反而会变慢?

问题原因

跟了一下代码发现了跟我们的设置有很大关系,因为我们设置的receiveTimeout=6000(1分钟)接受阻塞时间为1分钟。

ActiveMQ在消费时每个consumer会独占一个ThreadThead中通过consumer.receive()去阻塞,只有当consumer消费了maxMessagesPerTask个消息后,才会退出线程,由taskExecutor重新调度,maxMessagesPerTask这个值默认为10,可以通过下面代码得知:

@Override
public void initialize() {
    // Adapt default cache level.
    if (this.cacheLevel == CACHE_AUTO) {
        this.cacheLevel = (getTransactionManager() != null ? CACHE_NONE : CACHE_CONSUMER);
    }
    // Prepare taskExecutor and maxMessagesPerTask.
    synchronized (this.lifecycleMonitor) {
        if (this.taskExecutor == null) {
            this.taskExecutor = createDefaultTaskExecutor();
        }
        else if (this.taskExecutor instanceof SchedulingTaskExecutor &&
                ((SchedulingTaskExecutor) this.taskExecutor).prefersShortLivedTasks() &&
                this.maxMessagesPerTask == Integer.MIN_VALUE) {
            // TaskExecutor indicated a preference for short-lived tasks. According to
            // setMaxMessagesPerTask javadoc, we'll use 10 message per task in this case
            // unless the user specified a custom value.
            this.maxMessagesPerTask = 10;
        }
    }
    // Proceed with actual listener initialization.
    super.initialize();
}

ps. 我们使用的taskExecutor为:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor,因此上面代码走到else if中设置this.maxMessagesPerTask = 10;

如果消息写入很快的时候,你会发现消费的很快,只有当消息写入很慢的时候(比如说:1分钟写入不到10条)的时候,才会发现消息消费的有些慢

解决方案

如果有这类情况,可以调整receiveTime这个参数,具体参数设置多少合理自己去结合业务场景去权衡,可以根据消息写入的速度和写入量来设置该参数(maxMessagesPerTaskreceiveTimeout),调整这个参数有两种方式:

第一种使用JMS消费消息:

使用JMS消费消息时调整:jmsTemplatereceiveTimeout参数(以毫秒为单位,0表示阻塞接收不超时,默认值为0毫秒表示阻塞接受没有超时)

第二种使用listener-container消费消息:

使用jms:listener-container消费消息时调整:receive-timeout参数(以毫秒为单位, 默认值为1000毫秒(1秒); -1指示器根本没有超时。)

Trouble Shooting —— Docker Pull Image : Filesystem layer verification failed for digest sha256错误

问题现象

除了打包镜像的服务器上可以执行docker pull 192.168.0.34:5000/sample:latest以外,其它任何服务器执行此命令时,都会出现以下错误信息:

8b7054...: Verifying Checksum
Filesystem layer verification failed for digest sha256: 8b7054.....

这使得无法正常使用最新的sample镜像文件。

如果是按分析过程中的方式把8b7054文件夹迁移的话,docker会不断重试去拉取此文件信息,大概结果如下:

8b7054...: (..Retry 10 seconds)
Filesystem layer verification failed for digest sha256: 8b7054.....

分析过程

尝试在服务器上找日志,结果没有可用的日志。

/var/lib/registry下找该sha256的数据,能够找到,尝试移走该文件夹数据。结果执行docker pull命令时,依旧是报错。只好迁移回文件夹。

尝试在网络上寻找解决方案,有的说与源有关系,有的说与docker版本有关系,需要升级版本,大多都没有很好的解决。如果实在搞不定,估计

需要考虑这些方案了。

尝试删除所有sample开发版本相关的image,并重新打包镜像,结果问题依旧。

解决方案

docker build的过程中有很多选项可以使用,尝试将缓存关闭(默认否)、签名关闭(默认否)、清理过程文件(默认是)。

因此切换到jenkinsworkspace下,找到sample文件夹,执行以下命令:

docker build --rm=true --no-cache --disable-content-trust=true -t sample .
docker tag sample 192.168.0.34:5000/sample
docker push --disable-content-trust=true 192.168.0.34:5000/sample

编译打包过程没有任何错误,可以正常发布镜像到registry上。

于是,切换到其他服务器上去执行docker pull,结果一切正常。

没有checksum? 且没有原来失败的sha256 digest

看了下其他镜像成功过的pull日志,也是没有checksum。看来只有出现异常的时候,才会去checksum(待考证)

既然已经成功过,那还是用正常的方式去打包编译及下载。于是删除现有镜像文件,在jenkins上进行工程打包(原始逻辑)。

docker build -t sample:latest .
docker tag sample:latest 192.168.0.34:5000/sample:latest
docker push 192.168.0.34:5000/sample:latest

打包好后,在其它服务器上执行docker pull,一样可以正常使用了。

总结

问题最终通过docker创建镜像时增加了关闭缓存、关闭校验的参数(--rm=true --no-cache --disable-content-trust=true),然后构建出来的镜像pushregistry去重写这个镜像的最新digest值,再重新去掉这些参数再次构建镜像(恢复成正常构建镜像命令)后重新pushregistry。这个问题看样子是由于新构建的镜像无法修改registry上的digest值导致pull的时候报错。

Dubbo使用jsr303框架hibernate-validator遇到的问题

Published on:

Dubbo可以集成jsr303标准规范的验证框架,作为验证框架不二人选的hibernate-validator是大家都会经常在项目中使用的,但是在Dubbo使用是会发生下面这个问题。

问题描述

背景:使用springmvcrestful,使用dubbo做rpc,restful中调用大量的rpc,数据验证会在这两个地方,一个是restful层面,一个是rpc层面,restful层面使用springmvc默认的集成hibernate-validator来实现,参数开启验证只需要加入@Validated param

rpc层面也使用hibernate-validator实现,dubbo中开启validation也有两个方式,一个是在consumer端,一个是在provider端。

当我们在consumer端开启验证时:

<dubbo:reference id="serviceName" interface="com.domain.package.TestService" registry="registry" validation="true"/>

没有任何问题,可以拿到所有的数据校验失败数据。

当我们在provider端开启验证时:

<dubbo:service interface="com.domain.package.TestService" ref="serviceName" validation="true" />

会发生如下异常:

com.alibaba.dubbo.rpc.RpcException: Failed to invoke remote method: sayHello, provider: 

dubbo://127.0.0.1:20831/com.domain.package.TestService?application=dubbo-test-

rest&default.check=false&default.cluster=failfast&default.retries=0&default.timeout=1200000&default.version=1.0

.0&dubbo=2.6.1&interface=com.domain.package.TestService&methods=sayHello&pid=29268&register.ip=192.

168.6.47&side=consumer&timestamp=1524453157718, cause: com.alibaba.com.caucho.hessian.io.HessianFieldException: 

org.hibernate.validator.internal.engine.ConstraintViolationImpl.constraintDescriptor: 

'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
com.alibaba.com.caucho.hessian.io.HessianFieldException: 

org.hibernate.validator.internal.engine.ConstraintViolationImpl.constraintDescriptor: 

'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.logDeserializeError(JavaDeserializer.java:167)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize

(JavaDeserializer.java:408)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:273)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:200)
	at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:525)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2791)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2731)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2705)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
	at com.alibaba.com.caucho.hessian.io.CollectionDeserializer.readLengthList

(CollectionDeserializer.java:119)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2186)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize

(JavaDeserializer.java:404)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:273)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:200)
	at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:525)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2791)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2731)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2705)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
	at com.alibaba.dubbo.common.serialize.hessian2.Hessian2ObjectInput.readObject

(Hessian2ObjectInput.java:74)
	at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:90)
	at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:110)
	at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:88)
	at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:121)
	at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:82)
	at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:44)
	at com.alibaba.dubbo.remoting.transport.netty.NettyCodecAdapter$InternalDecoder.messageReceived

(NettyCodecAdapter.java:133)
	at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream

(SimpleChannelUpstreamHandler.java:70)
	at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
	at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:559)
	at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:268)
	at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:255)
	at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:88)
	at org.jboss.netty.channel.socket.nio.AbstractNioWorker.process(AbstractNioWorker.java:109)
	at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:312)
	at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:90)
	at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178)
	at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
	at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
	at java.lang.Thread.run(Thread.java:744)
Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException: 

'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:313)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:198)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2789)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2128)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2101)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize

(JavaDeserializer.java:404)
	... 43 more
Caused by: java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:309)
	... 50 more
Caused by: java.lang.NullPointerException
	at org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl.<init>

(ConstraintDescriptorImpl.java:158)
	at org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl.<init>

(ConstraintDescriptorImpl.java:211)
	... 55 more

问题分析

上面的问题从异常面来看已经很直观了,'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated,这个类无法实例化,那是什么原因导致它无法实例化呢?

Dubbo的序列化协议,默认是hessian,如果没有进行其他协议配置的话,默认使用的就是hessianhessian在反序列化时有个特点需要注意一下,它会在反序列化时取参数最少的构造器来创建对象,有的时候会有很多重载的构造器,因此会有一些参数直接给null,因此可能就会造成一些莫名其妙的问题,就像我们这个问题一样。

那这个问题如何解决呢?接着往下看

解决方案

由于这个是Hessian反序列化问题,因此与Dubbo的版本关系不大,为了验证这个我还专门使用apache dubbo 2.6.1版本测试了一下,问题依旧存在。

方法一:使用无参构造方法来创建对象

既然是hessian反序列化问题,而且它在反序列化时根据构造函数参数个数优先级来取参数最少的,那我们就可以增加一个无参的构造方法来解决这个问题。

但是有的时候我们使用的是第三方的包,不太好增加无参的构造方法,那怎么办的,我们能不能使用其他方法,继续往下看。

方法二:替换jsr303实现框架

既然hibernate-validatororg.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl这个类在使用hessian反序列化存在问题,那我们使用其他jsr303的框架来试试。

jsr303的实现框架有哪些?

  • org.hibernate : hibernate-validator : 5.2.4.Final
  • org.apache.bval : bval-jsr303 : 0.5
  • jersery

bval是apache的一个bean validator的实现,jersery是一个restful的框架为了满足自身的数据验证功能因此增加了jsr303的实现。

由于我们使用的springmvc构建restful因此这里就不考虑jersery,我们就从bval下手来试一试。

在进行了一番配置后(都有哪些配置?)

  • 增加bval包,现在版本是:0.5
<dependency>
	<groupId>org.apache.bval</groupId>
	<artifactId>bval-jsr303</artifactId>
	<version>0.5</version>
</dependency>
  • 将bval集成到spring框架中,作为spring的验证框架

这里有两种方式,一种xml配置,一种java config

xml方式:

<mvc:annotation-driven validator="validator"/>  
  
<!-- 数据验证 Validator bean -->  
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">  
    <property name="providerClass" value="org.apache.bval.jsr.ApacheValidationProvider" />  
</bean>  

java config方式: 重写mvcValidator方法

    @Override
    public Validator mvcValidator() {
        Validator validator = super.mvcValidator();
        if (validator instanceof LocalValidatorFactoryBean) {
            LocalValidatorFactoryBean lvfb = (LocalValidatorFactoryBean) validator;
            try {
                String className = "org.apache.bval.jsr303.ApacheValidationProvider";
                Class<?> clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
                lvfb.setProviderClass(clazz);
            }
            catch (ClassNotFoundException e) {
                //没有找到bval验证框架,走spring默认整合的验证框架:hibernate-validator
                //这里异常没有必要跑出去,直接吃掉
            }
        }
        return validator;
    }

启动后验证功能

但是不好的事情发生了,无法启动报错,错误如下:

java.lang.AbstractMethodError: org.apache.bval.jsr303.ConfigurationImpl.getDefaultParameterNameProvider()

Ljavax/validation/ParameterNameProvider;

经过对spring的资料查找,发现spring从4.0版本往后不在支持集成其他jsr303的框架了,只能使用hibernate-validator,我擦这个有点暴力了。即使自己实现一个jsr303框架也无法再spring中使用,除非不使用spring validator功能,直接使用自己的验证框架来进行验证,这样就无法使用@Validated param方式。

那这种方法只能放弃了。

方法三:修改hibernate-validator的原声类,修改Dubbo ValidationFilter,这也是我最终采用的方法

其实替换jsr303框架不能成功,替换序列化协议应该也可以避免这个问题,只不过替换协议这个一般在维护的项目中不太会选择这样的方式来动刀子,现在开发很多都是分布式服务,序列化反序列化已经无处不在了,因此我建议编写代码时都增加一个无参数的构造方法,养成这样的一个好习惯可以避免很多序列化反序列化框架的坑。而且还有那些有匿名内部类的这种在序列化反序列化也需要注意,不是所有的序列化反序列化框架都支持有匿名类,gson是支持的这个为测试过,我前面也写过一篇博文里面就主要说这个问题,可以查看:《Java中内部类使用注意事项,内部类对序列化与反序列化的影响》

有兴趣的可以看一下我们常用的序列化反序列化类库的一些使用中的注意事项,可以参考这篇文章:《java常用JSON库注意事项总结》

回归话题,上面的问题我们如何解决,最终我们采用重写javax.validation.ConstraintViolation<T>的实现类,替换掉hibernate-validationorg.hibernate.validator.internal.engine.ConstraintViolationImpl,因为ConstraintViolationImpl中有部分对象无法通过hessian反序列化。

我们最终的目标是不管是validation开启在provider端还是consumer端,调用方接收到的参数校验异常数据是一致的。

修改的代码已经提交到apache dubbo,具体查看Pull request:https://github.com/apache/incubator-dubbo/pull/1708

大概的代码如下:

增加类:DubboConstraintViolation实现javax.validation.ConstraintViolation接口

import java.io.Serializable;
import javax.validation.ConstraintViolation;
import javax.validation.Path;
import javax.validation.ValidationException;
import javax.validation.metadata.ConstraintDescriptor;
import com.alibaba.dubbo.common.logger.Logger;
import com.alibaba.dubbo.common.logger.LoggerFactory;


public class DubboConstraintViolation<T> implements ConstraintViolation<T>, Serializable {
    
    static final Logger logger = LoggerFactory.getLogger(DubboConstraintViolation.class.getName());

	private static final long serialVersionUID = -8901791810611051795L;

	private String interpolatedMessage;
    private Object value;
    private Path propertyPath;
    private String messageTemplate;
    private Object[] executableParameters;
    private Object executableReturnValue;
    private int hashCode;

    public DubboConstraintViolation() {
    }
    
    public DubboConstraintViolation(ConstraintViolation<T> violation) {
        this(violation.getMessageTemplate(), violation.getMessage(), violation.getInvalidValue(), violation.getPropertyPath(),
                violation.getExecutableParameters(), violation.getExecutableReturnValue());
    }

    public DubboConstraintViolation(String messageTemplate,
            String interpolatedMessage,
            Object value,
            Path propertyPath,
            Object[] executableParameters,
            Object executableReturnValue) {
        this.messageTemplate = messageTemplate;
        this.interpolatedMessage = interpolatedMessage;
        this.value = value;
        this.propertyPath = propertyPath;
        this.executableParameters = executableParameters;
        this.executableReturnValue = executableReturnValue;
        // pre-calculate hash code, the class is immutable and hashCode is needed often
        this.hashCode = createHashCode();
    }
    
    @Override
    public final String getMessage() {
        return interpolatedMessage;
    }

    @Override
    public final String getMessageTemplate() {
        return messageTemplate;
    }

    @Override
    public final T getRootBean() {
        return null;
    }

    @Override
    public final Class<T> getRootBeanClass() {
        return null;
    }

    @Override
    public final Object getLeafBean() {
        return null;
    }

    @Override
    public final Object getInvalidValue() {
        return value;
    }

    @Override
    public final Path getPropertyPath() {
        return propertyPath;
    }

    @Override
    public final ConstraintDescriptor<?> getConstraintDescriptor() {
        return null;
    }

    @Override
    public <C> C unwrap(Class<C> type) {
        if ( type.isAssignableFrom( ConstraintViolation.class ) ) {
            return type.cast( this );
        }
        throw new ValidationException("Type " + type.toString() + " not supported for unwrapping.");
    }

    @Override
    public Object[] getExecutableParameters() {
        return executableParameters;
    }

    @Override
    public Object getExecutableReturnValue() {
        return executableReturnValue;
    }

    @Override
    // IMPORTANT - some behaviour of Validator depends on the correct implementation of this equals method! (HF)

    // Do not take expressionVariables into account here. If everything else matches, the two CV should be considered
    // equals (and because of the scary comment above). After all, expressionVariables is just a hint about how we got
    // to the actual CV. (NF)
    public boolean equals(Object o) {
        if ( this == o ) {
            return true;
        }
        if ( o == null || getClass() != o.getClass() ) {
            return false;
        }

        DubboConstraintViolation<?> that = (DubboConstraintViolation<?>) o;

        if ( interpolatedMessage != null ? !interpolatedMessage.equals( that.interpolatedMessage ) : that.interpolatedMessage != null ) {
            return false;
        }
        if ( propertyPath != null ? !propertyPath.equals( that.propertyPath ) : that.propertyPath != null ) {
            return false;
        }
        if ( messageTemplate != null ? !messageTemplate.equals( that.messageTemplate ) : that.messageTemplate != null ) {
            return false;
        }
        if ( value != null ? !value.equals( that.value ) : that.value != null ) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        return hashCode;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append( "DubboConstraintViolation" );
        sb.append( "{interpolatedMessage='" ).append( interpolatedMessage ).append( '\'' );
        sb.append( ", propertyPath=" ).append( propertyPath );
        sb.append( ", messageTemplate='" ).append( messageTemplate ).append( '\'' );
        sb.append( ", value='" ).append( value ).append( '\'' );
        sb.append( '}' );
        return sb.toString();
    }

    // Same as for equals, do not take expressionVariables into account here.
    private int createHashCode() {
        int result = interpolatedMessage != null ? interpolatedMessage.hashCode() : 0;
        result = 31 * result + ( propertyPath != null ? propertyPath.hashCode() : 0 );
        result = 31 * result + ( value != null ? value.hashCode() : 0 );
        result = 31 * result + ( messageTemplate != null ? messageTemplate.hashCode() : 0 );
        return result;
    }

}

修改com.alibaba.dubbo.validation.filter.ValidationFilter异常处理的部分

这里的变更为捕捉javax.validation.ConstraintViolationException异常,对异常中的Set<ConstraintViolation<String>>数据进行转换,去掉无法反序列化的对象,具体代码如下:

public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
    if (validation != null && !invocation.getMethodName().startsWith("$")
            && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.VALIDATION_KEY))) {
        try {
            Validator validator = validation.getValidator(invoker.getUrl());
            if (validator != null) {
                validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
            }
        } catch (ConstraintViolationException e) {
            Set<ConstraintViolation<?>> set = null;
            //验证set中如果是hibernate-validation实现的类就处理,其他的实现类放过
            Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
            for (ConstraintViolation<?> v : constraintViolations) {
                if (!v.getClass().getName().equals("org.hibernate.validator.internal.engine.ConstraintViolationImpl")) {
                    return new RpcResult(e);
                } else {
                    if (set == null) set = new HashSet<ConstraintViolation<?>>();
                    set.add(new DubboConstraintViolation<>(v));
                }
            }
            return new RpcResult(new ConstraintViolationException(e.getMessage(), set));
        } catch (RpcException e) {
            throw e;
        } catch (Throwable t) {
            return new RpcResult(t);
        }
    }
    return invoker.invoke(invocation);
}

使用这个方法后,在provider端设置validation=trueconsumer端可以正常拿到所有校验数据的异常信息。

总结

我觉得这个方法并不是完美的方法,虽然这个问题是hibernate-validator框架的问题,hibernate-validator出生的年代分布式还不是特别的完善因此没有充分的考虑序列化反序列化问题也很正常,但是作为Dubbo框架在集成jsr303的时候也需要考虑这些问题。具体可以查看Apache DubboPull Requesthttps://github.com/apache/incubator-dubbo/pull/1708

单元测试以及代码覆盖率——Jenkins集成SonarQube、JaCoCo、Junit使用问题汇总

Published on:

当我们使用持续集成Jenkins的时候经常会结合一系列的插件使用,这里就说一下Jenkins集成Sonar做代码质量管理以及Junit(testng)JaCoCo做单元测试和覆盖率的时候遇到的问题。

前提

首先我们的工程使用maven构建,单元测试使用testng编写,在使用jenkins之前我们应该在本地使用maven调通所有的单元测试以及test coverage的问题。

我们使用maven-surefire-plugin来生成单元测试报告,使用jacoco-maven-plugin来生成test coverage报告。下面我给出以下我使用的标准配置

maven工程调通单元测试以及测试覆盖率报告生成

pom.xml的标准配置

<dependencies>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
	</dependency>
	<dependency>
		<groupId>org.testng</groupId>
		<artifactId>testng</artifactId>
		<version>6.4</version>
		<scope>test</scope>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<scope>test</scope>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>org.jacoco</groupId>
		<artifactId>jacoco-maven-plugin</artifactId>
		<version>0.8.1</version>
	</dependency>
</dependencies>

<build>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>2.5</version>
			<configuration>
				<skipTests>false</skipTests>
				<argLine>${argLine} -Dfile.encoding=UTF-8</argLine>
			</configuration>
		</plugin>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-deploy-plugin</artifactId>
			<configuration>
				<skip>false</skip>
			</configuration>
		</plugin>
		<plugin>
			<groupId>org.jacoco</groupId>
			<artifactId>jacoco-maven-plugin</artifactId>
			<version>0.8.1</version>
			<configuration>
				<skip>false</skip>
			</configuration>
			<executions>
				<execution>
					<goals>
						<goal>prepare-agent</goal>
					</goals>
				</execution>
				<execution>
					<configuration>
						<outputDirectory>${basedir}/target/coverage-reports</outputDirectory>
					</configuration>
					<id>report</id>
					<phase>test</phase>
					<goals>
						<goal>report</goal>
					</goals>
				</execution>
			</executions>
		</plugin>
	</plugins>
</build>

根据上面配置执行下来的报告生成的目录结构如下:

  • classes是源代码编译生成的字节码目录
  • coverage-reports是单元测试覆盖率报告生成目录
  • surefire-reports是单元测试报告生成目录
  • test-classes是单元测试代码编译生成的字节码目录
  • jacoco.exec是用于生成单元测试可执行文件

下面我说一下我们会遇到的常规问题

上步操作会遇到的常规问题

问题一:Tests are skipped.

[INFO] --- maven-surefire-plugin:2.5:test (default-test) @ tools ---
[INFO] Tests are skipped.

单元测试被跳过,这个可以通过maven-surefire-plugin插件的configuration来配置不跳过,如下配置:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<version>2.5</version>
	<configuration>
		<skipTests>false</skipTests>
	</configuration>
</plugin>

配置skipTests属性而不是skip属性这里需要注意一下,有很多人配置的skip属性

问题二:单元测试输出乱码

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running TestSuite
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
=====��һ��===============

=====��һ��===============

=====���¼���===============

单元测试输出信息乱码,这个可以通过maven-surefire-plugin插件的configuration来配置字符编码,如下配置:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<version>2.5</version>
	<configuration>
		<skipTests>false</skipTests>
		<argLine>-Dfile.encoding=UTF-8</argLine>
	</configuration>
</plugin>

到这里我们就可以去taget/surefire-reports目录下查看单元测试报告。

问题三:Skipping JaCoCo execution due to missing execution data file.

[INFO] --- jacoco-maven-plugin:0.8.1:report (report) @ tools ---
[INFO] Skipping JaCoCo execution due to missing execution data file.

jacoco执行被跳过,原因是没有找到jacoco可执行文件jacoco.exec

这个时候我们去target目录下是看不到jacoco.exec文件的,有的版本名字叫jacoco-junit.exec

理论上执行的时候会自动生成exec文件,但是为什么没有生成?我们看一下执行日志

[INFO] --- jacoco-maven-plugin:0.8.1:prepare-agent (default) @ tools ---
[INFO] argLine set to -javaagent:D:\\javatools\\mvnrepository\\org\\jacoco\\org.jacoco.agent\\0.8.1\\org.jacoco.agent-0.8.1-runtime.jar=destfile=D:\\javatools\\workspace\\framework\\tools\\target\\jacoco.exec

jacoco.exec的生成是根据-javaagent的方式来生成的,我们有可以看到jacoco-maven-plugin指定了argLine参数,但是为什么没有生效?

原因是我们上面指定过单元测试编码,使用的就是argLine参数,因此这个问题应该是上面的编码参数指定后没有带入插件添加的-javaagent参数,那如何解决?查看下面配置:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<version>2.5</version>
	<configuration>
		<skipTests>false</skipTests>
		<argLine>${argLine} -Dfile.encoding=UTF-8</argLine>
	</configuration>
</plugin>

argLine中增加变量${argLine}后面再增加自动以的参数

如果通过配置手动的指定jacoco.exec文件的生成路径也需要注意也可能会出现这个问题,生成exec的路径指定在哪里,report执行的时候就需要通过dataFile来指定exec的路径,让程序知道正确的exec路径,比如说:

<plugin>
	<groupId>org.jacoco</groupId>
	<artifactId>jacoco-maven-plugin</artifactId>
	<version>0.8.1</version>
	<configuration>
		<skip>false</skip>
		<destFile>${basedir}/target/coverage-reports/jacoco.exec</destFile>
	</configuration>
	<executions>
		<execution>
			<goals>
				<goal>prepare-agent</goal>
			</goals>
		</execution>
		<execution>
			<configuration>
				<dataFile>${basedir}/target/coverage-reports/jacoco.exec</dataFile>
				<outputDirectory>${basedir}/target/coverage-reports</outputDirectory>
			</configuration>
			<id>report</id>
			<phase>test</phase>
			<goals>
				<goal>report</goal>
			</goals>
		</execution>
	</executions>
</plugin>

上面通过configurationdestFile来自定义jacoco.exec的生成路径,下面在report的时候需要通过dataFile来指定对应的jacoco.exec的路径。

Jenkins使用JaCoCo plugin插件

首先去Jenkins上安装JaCoCo plugin插件,插件的安装就跳过了,插件安装好后,在job中如何配置?

这里需要注意的配置

  • Path to exec files: **/jacoco.exec 可执行文件路径
  • Path to class directories: 这个配置的是源代码编译后的字节码目录,也就是classes目录不是test-classes目录,如果有多个可以指定多个
  • Path to source directories: 这个配置的是源代码的目录,也就是src/main/java目录,如果有多个可以指定多个。

配置好之后执行job会看到如下的日志:

INFO: ------------------------------------------------------------------------
Injecting SonarQube environment variables using the configuration: SonarQube
[JaCoCo plugin] Collecting JaCoCo coverage data...
[JaCoCo plugin] **/jacoco.exec;**/classes;src/main/java; locations are configured
Injecting SonarQube environment variables using the configuration: SonarQube
Injecting SonarQube environment variables using the configuration: SonarQube
[JaCoCo plugin] Number of found exec files for pattern **/jacoco.exec: 1
[JaCoCo plugin] Saving matched execfiles:  /var/lib/jenkins/workspace/cc-framework-tools/target/coverage-reports/jacoco.exec
[JaCoCo plugin] Saving matched class directories for class-pattern: **/classes: 
[JaCoCo plugin]  - /var/lib/jenkins/workspace/cc-framework-tools/target/classes 5 files
[JaCoCo plugin] Saving matched source directories for source-pattern: src/main/java: 
[JaCoCo plugin] - /var/lib/jenkins/workspace/cc-framework-tools/src/main/java 5 files
[JaCoCo plugin] Loading inclusions files..
[JaCoCo plugin] inclusions: []
[JaCoCo plugin] exclusions: []
[JaCoCo plugin] Thresholds: JacocoHealthReportThresholds [minClass=0, maxClass=0, minMethod=0, maxMethod=0, minLine=0, maxLine=0, minBranch=0, maxBranch=0, minInstruction=0, maxInstruction=0, minComplexity=0, maxComplexity=0]
[JaCoCo plugin] Publishing the results..
[JaCoCo plugin] Loading packages..
[JaCoCo plugin] Done.
[JaCoCo plugin] Overall coverage: class: 50, method: 54, line: 48, branch: 40, instruction: 55
Finished: SUCCESS

出现上面日志就证明配置成功并且可以看到报告,如果出现下面的日志就证明配置的目录没有扫到classes,需要修改Path to class directories目录的配置

Overall coverage: class: 0, method: 0, line: 0, branch: 0, instruction: 0

最终结果如下图:

Jenkins使用Sonarqube plugin插件

首先去Jenkins上安装SonarQube plugin插件,插件的安装就跳过了,插件安装好后,在jenkins的系统配置中配置sonar服务器信息,如下

配置好后在job的配置中增加SonarQube的支持,如下

  • 在构建环境下添加Prepare SonarQube Scanner environment

  • 在构建下添加Execute SonarQube Scanner

  • Execute SonarQube Scanner中增加Analysis properties
# required metadata
# 项目key
sonar.projectKey=com.domian.package:projectName
# 项目名称
sonar.projectName=tools
# 项目版本,可以写死,也可以引用变量
sonar.projectVersion=${VER}
# 源文件编码
sonar.sourceEncoding=UTF-8
# 源文件语言
sonar.language=java
# path to source directories (required)
# 源代码目录,如果多个使用","分割 例如:mode1/src/main,mode2/src/main
sonar.sources=src/main
# 单元测试目录,如果多个使用","分割 例如:mode1/src/test,mode2/src/test
sonar.tests=src/test
# Exclude the test source
# 忽略的目录
#sonar.exclusions=*/src/test/**/*
# 单元测试报告目录
sonar.junit.reportsPath=target/surefire-reports
# 代码覆盖率插件
sonar.java.coveragePlugin=jacoco
# jacoco.exec文件路径
sonar.jacoco.reportPath=target/coverage-reports/jacoco.exec
# 这个没搞懂,官方示例是配置成jacoco.exec文件路径
sonar.jacoco.itReportPath=target/coverage-reports/jacoco.exec

具体的参数可以查看官方文档:《Analysis Parameters》

配置好之后执行job后去Sonar上只看到了单元测试的信息,没有看到单元测试覆盖率的信息,关于这个问题我们分析job执行的日志,如下:

问题一:No JaCoCo analysis of project coverage can be done since there is no class files.

16:01:17.455 INFO  - Sensor JaCoCoOverallSensor
16:01:17.470 INFO  - Analysing /var/lib/jenkins/workspace/cc-framework-tools/target/coverage-reports/jacoco.exec
16:01:17.481 INFO  - No JaCoCo analysis of project coverage can be done since there is no class files.
16:01:17.481 INFO  - Sensor JaCoCoOverallSensor (done) | time=26ms
16:01:17.482 INFO  - Sensor JaCoCoSensor
16:01:17.482 INFO  - No JaCoCo analysis of project coverage can be done since there is no class files.
16:01:17.482 INFO  - Sensor JaCoCoSensor (done) | time=0ms
16:01:17.482 INFO  - Sensor Code Colorizer Sensor

说的是没找到class文件所以jacoco不能进行分析,问题很明显是没有找到class类,难道它不是去maven标准的target/classes下找文件么?

但是找到了这篇文章:《Jenkins, JaCoCo, and SonarQube Integration With Maven》,看到里面在pom.xml中配置了一些参数给我了启发,发现有个参数sonar.binaries指定的是classes目录,可以插件的有些参数不兼容maven,在官方的配置中可以看到这样的字样: Not compatible with MaveCompatible with Maven,能看到有写参数兼容maven默认路径有些不兼容。

随后再官方文档中也找到了与jenkins继承的properties配置说明:《Triggering Analysis on Hudson Job》

# path to project binaries (optional), for example directory of Java bytecode
# java字节码目录
sonar.binaries=binDir

最终给出Execute SonarQube Scanner中的Analysis properties完成配置参数如下:

# required metadata
# 项目key
sonar.projectKey=com.domian.package:projectName
# 项目名称
sonar.projectName=tools
# 项目版本,可以写死,也可以引用变量
sonar.projectVersion=${VER}
# 源文件编码
sonar.sourceEncoding=UTF-8
# 源文件语言
sonar.language=java
# path to source directories (required)
# 源代码目录,如果多个使用","分割 例如:mode1/src/main,mode2/src/main
sonar.sources=src/main/java
# 单元测试目录,如果多个使用","分割 例如:mode1/src/test,mode2/src/test
sonar.tests=src/test/java
# java字节码目录
sonar.binaries=target/classes
# 单元测试报告目录
sonar.junit.reportsPath=target/surefire-reports
# 代码覆盖率插件
sonar.java.coveragePlugin=jacoco
# jacoco插件版本
jacoco.version=0.8.1
# jacoco.exec文件路径
sonar.jacoco.reportPath=target/coverage-reports/jacoco.exec

全部配置修改完后执行job后去Sonar上查看具体的信息如下:

TiDB使用笔记 —— 测试环境集群部署

Published on:
Tags: TiDB TiKV pd

TiDB是一个NewSql的分布式数据库,具体介绍我们引用官方的简介

简介

TiDB 是 PingCAP 公司受 Google Spanner / F1 论文启发而设计的开源分布式 NewSQL 数据库。

TiDB 具备如下 NewSQL 核心特性:

SQL支持(TiDB 是 MySQL 兼容的) 水平弹性扩展(吞吐可线性扩展) 分布式事务 跨数据中心数据强一致性保证 故障自恢复的高可用 海量数据高并发实时写入与实时查询(HTAP 混合负载) TiDB 的设计目标是 100% 的 OLTP 场景和 80% 的 OLAP 场景,更复杂的 OLAP 分析可以通过 TiSpark 项目来完成。

TiDB 对业务没有任何侵入性,能优雅的替换传统的数据库中间件、数据库分库分表等 Sharding 方案。同时它也让开发运维人员不用关注数据库 Scale 的细节问题,专注于业务开发,极大的提升研发的生产力。

我们来看一下TiDB的架构图

架构图

从架构图中可以看出TiDB的三大组件都支持水平扩展而且内部通信使用的是gRPC,关于TiDB和gRPC的那些事可以查看InfoQ的文章:《TiDB与gRPC的那点事》

TiDB使用的TiKV作为存储,官方建议至少TiKV使用ssd硬盘,如果条件好pd模块最好也使用ssd硬盘。

下来我们具体看一下三大组件分别都是干什么的

TiDB Server

TiDB Server 负责接收 SQL 请求,处理 SQL 相关的逻辑,并通过 PD 找到存储计算所需数据的 TiKV 地址,与 TiKV 交互获取数据,最终返回结果。 TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(如LVS、HAProxy 或 F5)对外提供统一的接入地址。

PD Server

Placement Driver (简称 PD) 是整个集群的管理模块,其主要工作有三个: 一是存储集群的元信息(某个 Key 存储在哪个 TiKV 节点);二是对 TiKV 集群进行调度和负载均衡(如数据的迁移、Raft group leader 的迁移等);三是分配全局唯一且递增的事务 ID。

PD 是一个集群,需要部署奇数个节点,一般线上推荐至少部署 3 个节点。

TiKV Server

TiKV Server 负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range (从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region 。TiKV 使用 Raft 协议做复制,保持数据的一致性和容灾。副本以 Region 为单位进行管理,不同节点上的多个 Region 构成一个 Raft Group,互为副本。数据在多个 TiKV 之间的负载均衡由 PD 调度,这里也是以 Region 为单位进行调度。

特性

可以无限水平扩展而且三大组件都是高可用,TiDB/TiKV/PD 这三个组件都能容忍部分实例失效,不影响整个集群的可用性。关于三大组件出现问题后如何恢复可以查看:《tidb-整体架构中的高可用章节》

官方的部署建议

TiDB使用的TiKV作为存储,官方建议至少TiKV使用ssd硬盘,如果条件好pd模块最好也使用ssd硬盘。

建议 4 台及以上,TiKV 至少 3 实例,且与 TiDB、PD 模块不位于同一主机。

组件 CPU 内存 本地存储 网络 实例数量(最低要求)
TiDB 8核+ 16 GB+ SAS, 200 GB+ 千兆网卡 1(可与 PD 同机器)
PD 8核+ 16 GB+ SAS, 200 GB+ 千兆网卡 1(可与 TiDB 同机器)
TiKV 8核+ 32 GB+ SSD, 200 GB+ 千兆网卡 3
- - - - 服务器总计 4

个人觉得这个使用的成本还是蛮高的。具体可以看《软、硬件环境要求》

测试部署

TiDB的部署方式还是蛮丰富的,可以使用Ansible在线以及离线的部署集群,TiDB-Ansible 是 PingCAP 基于 Ansible playbook 功能编写的集群部署工具。使用 TiDB-Ansible 可以快速部署一个完整的 TiDB 集群(包括 PD、TiDB、TiKV 和集群监控模块)。

TiDB同时也支持Docker部署方案,由于我们公司内网使用docker容器的方式管理所有服务,所以我这里使用docker方式部署。

我们使用Rancher来做企业级的容器管理平台,没有使用k8s、mesos来进行编排管理,使用的是Rancher自带的Cattle,Cattle不光有编排管理还包含了应用、服务、卷、负载均衡、健康检查、服务升级、dns服务、等功能,有兴趣的可以查看:《Rancher官方文档-Cattle》

在进行部署之前需要先去Docker官方镜像库中拉TiDB集群所需要的三大组件的镜像: Docker 官方镜像仓库

docker pull pingcap/tidb:latest
docker pull pingcap/tikv:latest
docker pull pingcap/pd:latest

这三个组件的镜像都不大,TiKV只有54MB,PD只有21MB,TiDB只有17MB

这个我需要说一下他们这块做的还是很不错的,将镜像压缩的都比较小,去除了很多无用的东西。

我们需要创建7个容器来部署一个TiDB集群:

容器 容器IP 宿主机IP 部署服务 数据盘挂载
PD1 10.42.59.28 192.168.18.108 PD1 /home/docker/TiDB
PD2 10.42.202.152 192.168.18.108 PD2 /home/docker/TiDB
PD3 10.42.214.245 192.168.18.108 PD3 /home/docker/TiDB
TiDB 10.42.188.35 192.168.18.109 TiDB /home/docker/TiDB
TiKV1 10.42.106.167 192.168.18.109 TiKV1 /home/docker/TiDB
TiKV2 10.42.34.97 192.168.18.109 TiKV2 /home/docker/TiDB
TiKV3 10.42.170.152 192.168.18.109 TiKV3 /home/docker/TiDB

用docker的好处就是资源可以压缩到最小,我6个容器可以放在一到两台虚机上

查看pd集群信息

http://192.168.18.108:2379/v2/members
http://192.168.18.108:2479/v2/members
http://192.168.18.108:2579/v2/members

返回信息以json格式,三台pd返回集群信息都是一样的

{"members":[{"id":"969b7171b723b804","name":"pd3","peerURLs":["http://192.168.18.108:2580"],"clientURLs":["http://192.168.18.108:2579"]},{"id":"d141f07798663b47","name":"pd2","peerURLs":["http://192.168.18.108:2480"],"clientURLs":["http://192.168.18.108:2479"]},{"id":"e5e987f33a60e672","name":"pd1","peerURLs":["http://192.168.18.108:2380"],"clientURLs":["http://192.168.18.108:2379"]}]}

具体的docker容器创建命令可以参考官方文档:《Docker部署方案》

TiDB支持mysql协议可以使用任意mysql客户端连接,默认安装好的集群使用mysql登录,端口:4000,用户名:root,密码为空,修改密码跟mysql修改密码方式完全一样。

SET PASSWORD FOR 'root'@'%' = 'xxx';

下面说几个我们必须要关心的东西。

事务隔离级别可以查看:《TiDB 事务隔离级别》

SQL语法没有什么变化,具体可以查看:《SQL语句语法》

SQL执行计划什么的都有跟使用mysql几乎一样,还增加了json的支持,可以设置字段列存储类型为json格式。

具体与MySQL有什么差异可以查看:《与MySQL兼容性对比》

历史数据回溯问题可以查看:《TiDB 历史数据回溯》

Binlog可以使用:《TiDB-Binlog 部署方案》

还有《备份与恢复》《数据迁移》

好了今天的大致介绍和测试环境集群搭建都到这里,后面会总结使用中遇到的问题。

MySql Lock wait timeout exceeded该如何处理?

Published on:

这个问题我相信大家对它并不陌生,但是有很多人对它产生的原因以及处理吃的不是特别透,很多情况都是交给DBA去定位和处理问题,接下来我们就针对这个问题来展开讨论。

Mysql造成锁的情况有很多,下面我们就列举一些情况:

  1. 执行DML操作没有commit,再执行删除操作就会锁表。
  2. 在同一事务内先后对同一条数据进行插入和更新操作。
  3. 表索引设计不当,导致数据库出现死锁。
  4. 长事物,阻塞DDL,继而阻塞所有同表的后续操作。

但是要区分的是Lock wait timeout exceededDead Lock是不一样。

  • Lock wait timeout exceeded:后提交的事务等待前面处理的事务释放锁,但是在等待的时候超过了mysql的锁等待时间,就会引发这个异常。
  • Dead Lock:两个事务互相等待对方释放相同资源的锁,从而造成的死循环,就会引发这个异常。

还有一个要注意的是innodb_lock_wait_timeoutlock_wait_timeout也是不一样的。

  • innodb_lock_wait_timeout:innodb的dml操作的行级锁的等待时间
  • lock_wait_timeout:数据结构ddl操作的锁的等待时间

如何查看innodb_lock_wait_timeout的具体值?

SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'

如何修改innode lock wait timeout的值?

参数修改的范围有Session和Global,并且支持动态修改,可以有两种方法修改:

方法一:

通过下面语句修改

set innodb_lock_wait_timeout=100;
set global innodb_lock_wait_timeout=100;

ps. 注意global的修改对当前线程是不生效的,只有建立新的连接才生效。

方法二:

修改参数文件/etc/my.cnf innodb_lock_wait_timeout = 50

ps. innodb_lock_wait_timeout指的是事务等待获取资源等待的最长时间,超过这个时间还未分配到资源则会返回应用失败; 当锁等待超过设置时间的时候,就会报如下的错误;ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction。其参数的时间单位是秒,最小可设置为1s(一般不会设置得这么小),最大可设置1073741824秒,默认安装时这个值是50s(默认参数设置)。

下面介绍在遇到这类问题该如何处理

问题现象

  • 数据更新或新增后数据经常自动回滚。
  • 表操作总报 Lock wait timeout exceeded 并长时间无反应

解决方法

  • 应急方法:show full processlist; kill掉出现问题的进程。 ps.有的时候通过processlist是看不出哪里有锁等待的,当两个事务都在commit阶段是无法体现在processlist上
  • 根治方法:select * from innodb_trx;查看有是哪些事务占据了表资源。 ps.通过这个办法就需要对innodb有一些了解才好处理

说起来很简单找到它杀掉它就搞定了,但是实际上并没有想象的这么简单,当问题出现要分析问题的原因,通过原因定位业务代码可能某些地方实现的有问题,从而来避免今后遇到同样的问题。

innodb_*表的解释

MysqlInnoDB存储引擎是支持事务的,事务开启后没有被主动Commit。导致该资源被长期占用,其他事务在抢占该资源时,因上一个事务的锁而导致抢占失败!因此出现 Lock wait timeout exceeded

下面几张表是innodb的事务和锁的信息表,理解这些表就能很好的定位问题。

innodb_trx ## 当前运行的所有事务 innodb_locks ## 当前出现的锁 innodb_lock_waits ## 锁等待的对应关系

下面对 innodb_trx 表的每个字段进行解释:

trx_id:事务ID。
trx_state:事务状态,有以下几种状态:RUNNING、LOCK WAIT、ROLLING BACK 和 COMMITTING。
trx_started:事务开始时间。
trx_requested_lock_id:事务当前正在等待锁的标识,可以和 INNODB_LOCKS 表 JOIN 以得到更多详细信息。
trx_wait_started:事务开始等待的时间。
trx_weight:事务的权重。
trx_mysql_thread_id:事务线程 ID,可以和 PROCESSLIST 表 JOIN。
trx_query:事务正在执行的 SQL 语句。
trx_operation_state:事务当前操作状态。
trx_tables_in_use:当前事务执行的 SQL 中使用的表的个数。
trx_tables_locked:当前执行 SQL 的行锁数量。
trx_lock_structs:事务保留的锁数量。
trx_lock_memory_bytes:事务锁住的内存大小,单位为 BYTES。
trx_rows_locked:事务锁住的记录数。包含标记为 DELETED,并且已经保存到磁盘但对事务不可见的行。
trx_rows_modified:事务更改的行数。
trx_concurrency_tickets:事务并发票数。
trx_isolation_level:当前事务的隔离级别。
trx_unique_checks:是否打开唯一性检查的标识。
trx_foreign_key_checks:是否打开外键检查的标识。
trx_last_foreign_key_error:最后一次的外键错误信息。
trx_adaptive_hash_latched:自适应散列索引是否被当前事务锁住的标识。
trx_adaptive_hash_timeout:是否立刻放弃为自适应散列索引搜索 LATCH 的标识。

下面对 innodb_locks 表的每个字段进行解释:

lock_id:锁 ID。
lock_trx_id:拥有锁的事务 ID。可以和 INNODB_TRX 表 JOIN 得到事务的详细信息。
lock_mode:锁的模式。有如下锁类型:行级锁包括:S、X、IS、IX,分别代表:共享锁、排它锁、意向共享锁、意向排它锁。表级锁包括:S_GAP、X_GAP、IS_GAP、IX_GAP 和 AUTO_INC,分别代表共享间隙锁、排它间隙锁、意向共享间隙锁、意向排它间隙锁和自动递增锁。
lock_type:锁的类型。RECORD 代表行级锁,TABLE 代表表级锁。
lock_table:被锁定的或者包含锁定记录的表的名称。
lock_index:当 LOCK_TYPE=’RECORD’ 时,表示索引的名称;否则为 NULL。
lock_space:当 LOCK_TYPE=’RECORD’ 时,表示锁定行的表空间 ID;否则为 NULL。
lock_page:当 LOCK_TYPE=’RECORD’ 时,表示锁定行的页号;否则为 NULL。
lock_rec:当 LOCK_TYPE=’RECORD’ 时,表示一堆页面中锁定行的数量,亦即被锁定的记录号;否则为 NULL。
lock_data:当 LOCK_TYPE=’RECORD’ 时,表示锁定行的主键;否则为NULL。

下面对 innodb_lock_waits 表的每个字段进行解释:

requesting_trx_id:请求事务的 ID。
requested_lock_id:事务所等待的锁定的 ID。可以和 INNODB_LOCKS 表 JOIN。
blocking_trx_id:阻塞事务的 ID。
blocking_lock_id:某一事务的锁的 ID,该事务阻塞了另一事务的运行。可以和 INNODB_LOCKS 表 JOIN。

锁等待的处理步骤

  • 直接查看 innodb_lock_waits 表
SELECT * FROM innodb_lock_waits;
  • innodb_locks 表和 innodb_lock_waits 表结合:
SELECT * FROM innodb_locks WHERE lock_trx_id IN (SELECT blocking_trx_id FROM innodb_lock_waits);
  • innodb_locks 表 JOIN innodb_lock_waits 表:
SELECT innodb_locks.* FROM innodb_locks JOIN innodb_lock_waits ON (innodb_locks.lock_trx_id = innodb_lock_waits.blocking_trx_id);
  • 查询 innodb_trx 表:
SELECT trx_id, trx_requested_lock_id, trx_mysql_thread_id, trx_query FROM innodb_trx WHERE trx_state = 'LOCK WAIT';
  • trx_mysql_thread_id 即kill掉事务线程 ID
SHOW ENGINE INNODB STATUS ;
SHOW PROCESSLIST ;

从上述方法中得到了相关信息,我们可以得到发生锁等待的线程 ID,然后将其 KILL 掉。 KILL 掉发生锁等待的线程。

kill ID;

RediSearch基于Redis的高性能全文搜索引擎,资料整理

最近在参考CQRS DDD架构来进行公司的库存中心重构设计,在CQRS架构中需要一个in-memory的方式快速修改库存在通过消息驱动异步更新到DB,也就是说内存的数据是最新的,DB的数据是异步持久化的,在某一个时刻内存和DB的数据是存在不一致的,但是满足最终一致性。

这样我们就需要内存当作前置DB在使用,因此不单纯的只满足修改数据,还需要满足Query的要求,内存结构的数据Query是比较麻烦的,它不像DB那样已经实现好了索引检索,需要我们自己来设计Key的机构和搜索索引的构建。

当然行业里也有这样的做法,对数据修改的时候双写到内存(Redis)和ElasticSearch再异步到DB,这样Query全部走向ElasticSearch,但是我觉得这样做的复杂度会增加很多,所以就在看如何基于Redis来设计一个搜索引擎。

看到了RedisLabs团队开发的基于Redis的搜索引擎:RediSearch

RediSearch

Github: RediSearch

官方站点

官方给出的描述

Redisearch implements a search engine on top of redis, but unlike other redis search libraries, it does not use internal data structures like sorted sets.
Inverted indexes are stored as a special compressed data type that allows for fast indexing and search speed, and low memory footprint.
This also enables more advanced features, like exact phrase matching and numeric filtering for text queries, that are not possible or efficient with traditional redis search approaches.

主要特点

高性能的全文搜索引擎(Faster, in-memory, highly available full text search),可作为Redis Module运行在Redis上。但是它与其他Redis搜索库不同的是,它不使用Redis内部数据结构,例如:集合、排序集(ps.后面会写一篇基于Redis的数据结构来设计搜索引擎),Redis原声的搜索还是有很大的局限性,简单的分词搜索是可以满足,但是应用到复杂的场景就不太适合。

  • Full-Text indexing of multiple fields in documents.
  • Incremental indexing without performance loss.
  • Document ranking (provided manually by the user at index time).
  • Field weights.
  • Complex boolean queries with AND, OR, NOT operators between sub-queries.
  • Prefix matching in full-text queries.
  • Auto-complete suggestions (with fuzzy prefix suggestions)
  • Exact Phrase Search.
  • Stemming based query expansion in many languages (using Snowball).
  • Support for logographic (Chinese, etc.) tokenization and querying (using Friso)
  • Limiting searches to specific document fields (up to 128 fields supported).
  • Numeric filters and ranges.
  • Geographical search utilizing redis’ own GEO commands.
  • Supports any utf-8 encoded text.
  • Retrieve full document content or just ids.
  • Automatically index existing HASH keys as documents.
  • Document Deletion (Update can be done by deletion and then re-insertion).
  • Sortable properties (i.e. sorting users by age or name).

下面是中文版本

  • 多个字段的文档的全文索引。
  • 没有性能损失增量索引。
  • 文档排名(由用户提供手动指数时间)。
  • 字段权重。
  • 在子查询之间使用AND,OR,NOT运算符进行复杂的布尔查询。
  • 前缀匹配全文查询。
  • 自动完成建议以模糊前缀(建议)
  • 准确短语搜索。
  • 阻止基于查询扩展多种语言(使用Snowball)。
  • 支持语标的(中国等)标记和查询(使用Friso)
  • 将搜索限制在特定的文档字段(128字段支持)。
  • 数字过滤器和范围。
  • 利用redis自己的GEO命令进行地理搜索。
  • 支持任何utf-8编码的文本。
  • 获取完整的文档内容或者只是id。
  • 自动索引现有HASH keys文件。
  • 文档删除(更新可以通过删除然后re-insertion)。
  • 可排序属性(即按年龄或名称对用户进行排序)。

集群

当然还支持分布式集群,只不过集群还是试验阶段还不建议正式应用到企业级应用上。

暂不支持

  • Spelling correction(拼写更正)
  • Aggregations(集合)

支持的Client类库

Official (Redis Labs) and community Clients:

Language Library Author License Comments
Python redisearch-py Redis Labs BSD Usually the most up-to-date client library
Java JRediSearch Redis Labs BSD -
Go redisearch-go Redis Labs BSD Incomplete API
JavaScript RedRediSearch Kyle J. Davis MIT Partial API, compatible with Reds
C# NRediSearch Marc Gravell MIT Part of StackExchange.Redis
PHP redisearch-php Ethan Hann MIT -
Ruby on Rails redi_search_rails Dmitry Polyakovsky MIT -
Ruby redisearch-rb Victor Ruiz MIT -

类库支持的还算丰富,可以尝试使用一下。

性能

性能对比是以ElasticSearch、Solr来进行对比,官方的benchmark数据,benchmark程序地址

总结:

从数据上看,使用RediSearch的吞吐量高、延迟低,但是相比于ElasticSearch和Solr支持的特性上还有些欠缺比如:中文的模糊搜索支持的不是很好,但是其性能很高在某些场景是可以作为搜索引擎的替代方案来试用。

案例资料:

  1. 利用RediSearch构建高效实时搜索案例
  2. 一步步实现 Redis 搜索引擎
  3. 我们做了一个支持全文搜索和关系查询的 Redis

上述就是关于RediSearch的资料整理,后面会尝试使用它来构建搜索引擎,会记录使用过程经历。

Trouble Shooting —— CAS Server集群环境下报错:Server redirected too many times (20)

当我们使用cas做单点登录的时候往往会使用集群方式部署,不管是cas server或者是接入的app server都会采用集群的方式部署。

在对cas server做集群实现无状态化,需要注意一下几点,也是我上一篇cas遇到的TGC验证问题中总结出来的:

  1. casticket需要做到集中存储,可以使用redisjpa、或者其他方式,这个官方文章上有详细介绍:ticket-registry
  2. cassession信息需要做到集中存储,如果使用的是tomcat可以使用TomcatRedisSessionMananger插件来通过redissession集中存储。
  3. 还有一个就是上面遇到的问题,客户端cookie信息:TGCTGC采用cookie方式存在客户端,因此需要开启会话保持,使得相同客户端每次都会被路由到同一个cas server上去做TGC验证。
  4. 最后一个就是需要接入ssoclient应用端的session信息也需要做集中存储,因此cas server会和client进行通信去验证ticket,验证完后会生成信息并存储到sesson中,因此也需要使用TomcatRedisSessionMananger插件来通过redissession集中存储。
  5. cas server端和接入的app服务端需要保证网络通畅。

cas使用总结博文目录

最近cas遇到的问题我都总结到了blog中,这里整理一下目录如下:

接下来我们就说一下这次遇到的问题。

问题现象

通过上面的方式可以将cas server做到集群无状态化,但是避免不了其他的问题,下面就是最近与到的问题,现象是这样的,一部分人可以正常登陆,一部分人登陆时报错,错误如下:

2018-03-23 10:33:22.768 [http-nio-7051-exec-1] ERROR org.jasig.cas.client.util.CommonUtils - Server redirected too many  times (20)
java.net.ProtocolException: Server redirected too many  times (20)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1637) ~[na:1.7.0_79]
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254) ~[na:1.7.0_79]
	at org.jasig.cas.client.util.CommonUtils.getResponseFromServer(CommonUtils.java:393) ~[cas-client-core-3.3.3.jar:3.3.3]
	at org.jasig.cas.client.validation.AbstractCasProtocolUrlBasedTicketValidator.retrieveResponseFromServer(AbstractCasProtocolUrlBasedTicketValidator.java:45) [cas-client-core-3.3.3.jar:3.3.3]
	at org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator.validate(AbstractUrlBasedTicketValidator.java:200) [cas-client-core-3.3.3.jar:3.3.3]
	at org.springframework.security.cas.authentication.CasAuthenticationProvider.authenticateNow(CasAuthenticationProvider.java:140) [spring-security-cas-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.cas.authentication.CasAuthenticationProvider.authenticate(CasAuthenticationProvider.java:126) [spring-security-cas-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:156) [spring-security-core-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.cas.web.CasAuthenticationFilter.attemptAuthentication(CasAuthenticationFilter.java:242) [spring-security-cas-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:195) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.jasig.cas.client.session.SingleSignOutFilter.doFilter(SingleSignOutFilter.java:100) [cas-client-core-3.3.3.jar:3.3.3]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at com.bstek.bdf2.core.security.filter.PreAuthenticatedProcessingFilter.doFilter(PreAuthenticatedProcessingFilter.java:41) [scm-bdf2-core-1.1.0-SNAPSHOT.jar:na]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:105) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:125) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at com.bstek.bdf2.core.security.filter.ContextFilter.doFilter(ContextFilter.java:36) [scm-bdf2-core-1.1.0-SNAPSHOT.jar:na]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344) [spring-web-4.0.0.RELEASE.jar:4.0.0.RELEASE]
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261) [spring-web-4.0.0.RELEASE.jar:4.0.0.RELEASE]

从异常的描述来看是服务器端多次redirected超过了20次导致的问题,什么原因会造成这个问题?

cas单点登录过程剖析

cas的单点登录的过程大致是这样的。

第一步:访问app地址,例如:https://app.domain.com,app端的cas-client-core会判断是否已经登录,如果没有登录会重定向到如下地址:https://login.domain.com/login?service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_

第二步:当重定向到cas登录页面后,我们输入用户名密码,cas server端会进行如下操作

  • 先进行AUTHENTICATION过程,这个过程是验证我们的用户名密码是否正确,会输出如下日志:
2018-03-23 14:58:01,429 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: admin
WHAT: Supplied credentials: [admin]
ACTION: AUTHENTICATION_SUCCESS
APPLICATION: CAS
WHEN: Fri Mar 23 14:58:01 HKT 2018
CLIENT IP ADDRESS: xx.xx.xx.xx
SERVER IP ADDRESS: xx.xx.xx.xx
=============================================================
  • AUTHENTICATION通过以后会生成TGT(TICKET_GRANTING_TICKET),这个是换取服务票据的预授票据,并且将TGT保存起来,我这里使用的是jpa方式保存到db,会输出如下日志:
=============================================================
WHO: admin
WHAT: TGT-***********************************************1VX72iaQBZ-077adac8d80f
ACTION: TICKET_GRANTING_TICKET_CREATED
APPLICATION: CAS
WHEN: Fri Mar 23 14:58:01 HKT 2018
CLIENT IP ADDRESS: 10.42.37.135
SERVER IP ADDRESS: 10.42.185.88
=============================================================

>
Hibernate: insert into TICKETGRANTINGTICKET (NUMBER_OF_TIMES_USED, CREATION_TIME, EXPIRATION_POLICY, LAST_TIME_USED, PREVIOUS_LAST_TIME_USED, AUTHENTICATION, EXPIRED, PROXIED_BY, SERVICES_GRANTED_ACCESS_TO, ticketGrantingTicket_ID, TYPE, ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'TGT', ?)
  • TGT生成完后会生成ST(SERVICE_TICKET),这个是服务票据,是授权这个服务的票据,并且会将ST保存起来和更新TGT信息,我这里使用的是jpa方式保存到db,会输出如下日志:
2018-03-23 14:58:01,504 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: admin
WHAT: ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f for https://app.domain.com/cas_security_check_
ACTION: SERVICE_TICKET_CREATED
APPLICATION: CAS
WHEN: Fri Mar 23 14:58:01 HKT 2018
CLIENT IP ADDRESS: xx.xx.xx.xx
SERVER IP ADDRESS: xx.xx.xx.xx
=============================================================

>
Hibernate: insert into SERVICETICKET (NUMBER_OF_TIMES_USED, CREATION_TIME, EXPIRATION_POLICY, LAST_TIME_USED, PREVIOUS_LAST_TIME_USED, FROM_NEW_LOGIN, TICKET_ALREADY_GRANTED, SERVICE, ticketGrantingTicket_ID, TYPE, ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, 'ST', ?)
Hibernate: update TICKETGRANTINGTICKET set NUMBER_OF_TIMES_USED=?, CREATION_TIME=?, EXPIRATION_POLICY=?, LAST_TIME_USED=?, PREVIOUS_LAST_TIME_USED=?, AUTHENTICATION=?, EXPIRED=?, PROXIED_BY=?, SERVICES_GRANTED_ACCESS_TO=?, ticketGrantingTicket_ID=? where ID=?

这个时候服务端生成的票据就完成了,会将ST信息生成TGC(TICKET_GRANTING_COOKIE)返回给app端。

第三步:app端接收到cas server端的返回,TGC会直接写入到浏览器cookie中,app端会再发起一次ST验证,这个过程是在app的后端发起请求的,url如下:

https://login.domain.com/serviceValidate?ticket=ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f&service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_

第四步:cas server端收到service validate请求后会验证ST和TGC是否合法,并且验证TGC的时候cas server需要开启会话保持,让请求发送到生成TGC的机器上去,因为TGC中保存生成的服务端地址,具体问题我前面分析过查看:《Trouble Shooting —— CAS Server集群环境下TGC验证问题排查,需要开启会话保持》,cas server验证成功后会输出如下的日志:

2018-03-23 14:58:01,578 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: admin
WHAT: ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f
ACTION: SERVICE_TICKET_VALIDATED
APPLICATION: CAS
WHEN: Fri Mar 23 14:58:01 HKT 2018
CLIENT IP ADDRESS: xx.xx.xx.xx
SERVER IP ADDRESS: xx.xx.xx.xx
=============================================================

ps.出现下面日志表示验证失败

2018-03-23 14:58:01,580 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: audit:unknown
WHAT: ST-154-YA6KibaqHpOMGXbluz7V-077adac8d80f
ACTION: SERVICE_TICKET_VALIDATE_FAILED
APPLICATION: CAS
WHEN: Fri Mar 23 14:58:01 HKT 2018
CLIENT IP ADDRESS: xx.xx.xx.xx
SERVER IP ADDRESS: xx.xx.xx.xx
=============================================================

第五步:app后端接收到cas server端service验证成功的返回后,会生成session并且与TG进行关系绑定,绑定信息会保存起来,这里需要注意的是如果是集群环境需要保存到redis或者其他统一存储的地方。,app后端接收验证成功后的输出日志如下:

2018-03-23 14:58:01.531 [http-apr-8080-exec-1] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Constructing validation url: https://login.domain.com/serviceValidate?ticket=ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f&service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_
2018-03-23 14:58:01.531 [http-apr-8080-exec-1] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Retrieving response from server.
2018-03-23 14:58:01.602 [http-apr-8080-exec-1] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Server response: <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
    <cas:authenticationSuccess>
        <cas:user>admin</cas:user>
    </cas:authenticationSuccess>
</cas:serviceResponse>

输出以上信息就是验证成功。到这里cas server端的所有验证都完成了。

ps.出现下面日志表示app后端接收到的是验证失败返回信息

2018-03-23 14:58:02.295 [http-bio-7051-exec-6] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Constructing validation url: https://login.domain.com/serviceValidate?ticket=ST-154-YA6KibaqHpOMGXbluz7V-077adac8d80f&service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_
2018-03-23 14:58:02.295 [http-bio-7051-exec-6] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Retrieving response from server.
2018-03-23 14:58:02.830 [http-bio-7051-exec-6] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Server response: <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
    <cas:authenticationFailure code="INVALID_TICKET">Ticket &#39;ST-154-YA6KibaqHpOMGXbluz7V-077adac8d80f&#39; not recognized</cas:authenticationFailure>
</cas:serviceResponse>

第六步:app端登录成功进入主页面。

根据这个流程我们再来分析上面的异常是那个环节出现了问题。

问题分析

首先上面的异常是app的后端出现的异常,app后端发起请求是在cas server生成完ticket之后才发起的,并且发起的是service validate验证请求,这个请求导致重定向超过20次。

而且还有一个重要的信息就是,一部分人可以正常登录,一部分人不能登录,我们部署的结构是2台cas server,2台app服务。

通过日志排查,2台app服务,其中一台没有出现过一场,另外一台爆出异常。这个时候问题已经有些明朗了,当负载均衡路由到出错的这台服务时,后台服务发起service validate验证时出现了问题,那接下来就让我们对比两台服务器上的配置。

我们采用的是阿里云的SLB映射到后台的nginx,app的后台服务要和cas server通信那首先网络需要是通的,理论上网络应该是没问题的,但是为了验证问题,我们就从网络这块开始排查。

因为我们使用的是阿里云而且app服务没有开通外网,app后天和cas服务通信走的是内网的SLB,接下来我们就ping一下登录地址看一下返回的slb地址是否相同。

两台机器上ping login.domain.com ,果然返回的ip不一致,其中报错的那台机器返回的是本机ip,奥这就是问题的根源,cat /etc/hosts果然域名映射的ip不一致,应该是运维配置失误导致的问题。

通过修改host配置之后再次验证错误解决。

问题总结

最终定位的到的问题感觉很白痴的问题,是因为运维配置失误导致,但是值得回味的是,通过这个问题我们对cas的单点登录机制理解的更加深刻,这就是一种收获,往往通过繁琐的分析后定位到的问题都很easy,所以当我们分析问题、定位问题的时候一定要先理解其中的原理,再结合现象去一步一步分析,这是仔细和关注度是否全面的一种考验。好了问题就说到这里,希望能够帮助到需要的人。

世界和平、Keep Real!

Subversion库如何全文检索代码?

Published on:

现在是Git流行的年代,在Git的套件里想要全文检索代码也有很多方案,Git也支持命令直接检索代码,但是当使用svn的用户代码检索应该如何处理呢?

在回答前面问题之前我们还要搞清楚另外一个问题,我们为什么要检索代码?

有的时候我们想从所有的代码库去寻找使用相同方法的代码,常规做法就是checkout下来所有的项目,然后通过IDE工具去关联检索使用到某个方法的代码,但是这样做比较耗费时间而且当项目过多IDE不一定能扛得住。还有的时候我们想从规范角度去check开发人员写的代码是否有违规的或者有问题的,就可以通过检索去寻找,当然规范的check有更好的工具,可以使用scm工具sonarcheck代码它整合了很多check模版。

鉴于上面种种的原因对代码做检索还是很有必要的,接下来我们就说一下使用svn时如何全文检索代码。

我们可以先说一个思路,把代码灌入elasticsearchlucenesolr,然后通过ui去搜索这是一条可行的路子。

这两天发现了一个工具svnquery很好用,它使用ASP.net开发,采用Lucene生成索引,提供GUIWEB工具通过索引文件来检索代码。

svnquery官网

它提供三个程序,一个svnindex用于通过svn库生成索引目录

SvnIndex.exe %aciton% %index_path% %svn_path% -u 用户名 -p 密码

ps. action包括createupdate,更新和修改

执行后会生成一个索引目录,可以通过svnfind工具可以选择索引目录来进行代码搜索,svnfind是一个GUI工具。

还可以通过SvnWebQuery来进行代码搜索,SvnWebQuery是一个.NETweb程序需要放入IIS服务器来使用

引用官网的两张图

唯一的缺点就是需要一个库一个库的生成索引,没有批量生成svn路径下所有有权限的库,如果有这个功能我个人觉得就完美了。

好了工具介绍到这里,如果有用svn的想对代码进行检索的可以使用这个工具。

Zookeeper常用命令与注意事项

Published on:
Tags: zookeeper

Zookeeper在互联网行业和分布式环境下是最常用的集群协调工具,那我们今天就对Zookeeper的常用命令和使用注意事项进一步说明,在这之前我们先看一下Zookeeper是什么,它能做什么?

Zookeeper是什么?

ZooKeeper是一个开源的分布式应用程序协调服务,是GoogleChubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

它的这些特性可以让我们在很多场景下使用它,可以用它做注册中心、分布式锁、选举、队列等。

Zookeeper的原理

ZooKeeper是以Fast Paxos算法为基础的,Paxos 算法存在活锁的问题,即当有多个proposer交错提交时,有可能互相排斥导致没有一个proposer能提交成功,而Fast Paxos作了一些优化,通过选举产生一个leader (领导者),只有leader才能提交proposer,具体算法可见Fast Paxos。因此,要想弄懂ZooKeeper首先得对Fast Paxos有所了解

ZooKeeper的基本运转流程:

  1. 选举Leader
  2. 同步数据。
  3. 选举Leader过程中算法有很多,但要达到的选举标准是一致的。
  4. Leader要具有最高的执行ID,类似root权限。
  5. 集群中大多数的机器得到响应并接受选出的Leader

Zookeeper数据结构

与普通的文件系统极其类似,如下:

其中每个节点称为一个znode. 每个znode由3部分组成:

  • stat. 此为状态信息, 描述该znode的版本, 权限等信息.
  • data. 与该znode关联的数据.
  • children. 该znode下的子节点.

Zookeeper节点类型

  • persistentpersistent节点不和特定的session绑定, 不会随着创建该节点的session的结束而消失, 而是一直存在, 除非该节点被显式删除.
  • ephemeralephemeral节点是临时性的, 如果创建该节点的session结束了, 该节点就会被自动删除. ephemeral节点不能拥有子节点. 虽然ephemeral节点与创建它的session绑定, 但只要该该节点没有被删除, 其他session就可以读写该节点中关联的数据. 使用-e参数指定创建ephemeral节点.
  • sequence: 严格的说, sequence并非节点类型中的一种. sequence节点既可以是ephemeral的, 也可以是persistent的. 创建sequence节点时, ZooKeeper server会在指定的节点名称后加上一个数字序列, 该数字序列是递增的. 因此可以多次创建相同的sequence节点, 而得到不同的节点. 使用-s参数指定创建sequence节点.

Zookeeper常用命令

启动服务

[app@iZbp1dijzcfg8m0bcqfv9yZ zookeeper]$ ./bin/zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /usr/local/servers/zookeeper/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED

查看当前zk节点状态

[zk@iZbp1dijzcfg8m0bcqfv9yZ bin]$ ./zkServer.sh status
JMX enabled by default
Using config: /usr/local/servers/zookeeper/zookeeper/bin/../conf/zoo.cfg
Mode: standalone

ps. standalone代表单机模式,

[zk@iZ23np2fk60Z bin]$ ./zkServer.sh status
JMX enabled by default
Using config: /usr/local/zookeeper/bin/../conf/zoo.cfg
Mode: leader

ps. 集群模式下会显示的状态,leader节点,集群中其他机器会从leader节点同步数据

[zk@iZ237ydkhyiZ bin]$ ./zkServer.sh status
JMX enabled by default
Using config: /usr/local/zookeeper/bin/../conf/zoo.cfg
Mode: follower

ps. 集群模式下会显示的状态,follower节点在启动过程中会从leader节点同步所有数据

连接服务

[app@iZbp1dijzcfg8m0bcqfv9yZ zookeeper]$ ./bin/zkCli.sh -server ip:port  

ps. 不写ip端口默认连接本机服务.

查看节点信息

[zk: localhost:2181(CONNECTED) 0] ls /
[seq, dubbo, disconf, otter, pinpoint-cluster, zookeeper]

查看指定node的子node

[zk: localhost:2181(CONNECTED) 3] ls /zookeeper
[quota]

创建一个普通节点

[zk: localhost:2181(CONNECTED) 6] create /hello world
Created /hello

获取hello节点的数据与状态

[zk: localhost:2181(CONNECTED) 8] get /hello
world
cZxid = 0x262ea76
ctime = Wed Mar 21 14:39:12 CST 2018
mZxid = 0x262ea76
mtime = Wed Mar 21 14:39:12 CST 2018
pZxid = 0x262ea76
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

删除hello节点

[zk: localhost:2181(CONNECTED) 9] delete /hello
[zk: localhost:2181(CONNECTED) 10] get /hello
Node does not exist: /hello

ps. 使用delete命令可以删除指定znode. 当该znode拥有子znode时, 必须先删除其所有子znode, 否则操作将失败. rmr命令可用于代替delete命令, rmr是一个递归删除命令, 如果发生指定节点拥有子节点时, rmr命令会首先删除子节点.

znode节点的状态信息

使用get命令获取指定节点的数据时, 同时也将返回该节点的状态信息, 称为Stat. 其包含如下字段:

  • czxid. 节点创建时的zxid.
  • mzxid. 节点最新一次更新发生时的zxid.
  • ctime. 节点创建时的时间戳.
  • mtime. 节点最新一次更新发生时的时间戳.
  • dataVersion. 节点数据的更新次数.
  • cversion. 其子节点的更新次数.
  • aclVersion. 节点ACL(授权信息)的更新次数.
  • ephemeralOwner. 如果该节点为ephemeral节点, ephemeralOwner值表示与该节点绑定的session id. 如果该节点不是ephemeral节点, ephemeralOwner值为0. 至于什么是ephemeral节点, 请看后面的讲述.
  • dataLength. 节点数据的字节数.
  • numChildren. 子节点个数.

zxid

znode节点的状态信息中包含czxid和mzxid, 那么什么是zxid呢? ZooKeeper状态的每一次改变, 都对应着一个递增的Transaction id, 该id称为zxid. 由于zxid的递增性质, 如果zxid1小于zxid2, 那么zxid1肯定先于zxid2发生. 创建任意节点, 或者更新任意节点的数据, 或者删除任意节点, 都会导致Zookeeper状态发生改变, 从而导致zxid的值增加.

session

在client和server通信之前, 首先需要建立连接, 该连接称为session. 连接建立后, 如果发生连接超时, 授权失败, 或者显式关闭连接, 连接便处于CLOSED状态, 此时session结束.

创建不同类型的节点

节点的类型前面已经讲过。

创建一个临时节点

[zk: localhost:2181(CONNECTED) 12] create -e /hello world   
Created /hello
[zk: localhost:2181(CONNECTED) 13] get /hello
world
cZxid = 0x262ea78
ctime = Wed Mar 21 14:45:23 CST 2018
mZxid = 0x262ea78
mtime = Wed Mar 21 14:45:23 CST 2018
pZxid = 0x262ea78
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x15c150a650f066c
dataLength = 5
numChildren = 0

创建一个序列节点

[zk: localhost:2181(CONNECTED) 14] create -s /hello1 world
Created /hello10000000007
[zk: localhost:2181(CONNECTED) 15] create -s /hello1 world
Created /hello10000000008
[zk: localhost:2181(CONNECTED) 16] ls /
[hello, dubbo, otter, zookeeper, seq, disconf, hello10000000007, hello10000000008, pinpoint-cluster]
[zk: localhost:2181(CONNECTED) 17] get /hello10000000007
world
cZxid = 0x262ea7e
ctime = Wed Mar 21 14:47:51 CST 2018
mZxid = 0x262ea7e
mtime = Wed Mar 21 14:47:51 CST 2018
pZxid = 0x262ea7e
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

watch

watch的意思是监听感兴趣的事件. 在命令行中, 以下几个命令可以指定是否监听相应的事件.

ls命令

ls命令. ls命令的第一个参数指定znode, 第二个参数如果为true, 则说明监听该znode的子节点的增减, 以及该znode本身的删除事件.

[zk: localhost:2181(CONNECTED) 27] create /hello world
Created /hello
[zk: localhost:2181(CONNECTED) 28] ls /hello true
[]
[zk: localhost:2181(CONNECTED) 29] create /hello/test item001

WATCHER::

WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/hello
Created /hello/test

get命令

get命令. get命令的第一个参数指定znode, 第二个参数如果为true, 则说明监听该znode的更新和删除事件.

[zk: localhost:2181(CONNECTED) 30] get /hello true
world
cZxid = 0x262ef5d
ctime = Wed Mar 21 14:52:16 CST 2018
mZxid = 0x262ef5d
mtime = Wed Mar 21 14:52:16 CST 2018
pZxid = 0x262ef5e
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 1
[zk: localhost:2181(CONNECTED) 31] create /hello/test1 item001
Created /hello/test1
[zk: localhost:2181(CONNECTED) 32] rmr /hello

WATCHER::

WatchedEvent state:SyncConnected type:NodeDeleted path:/hello

stat命令

stat命令. stat命令用于获取znode的状态信息. 第一个参数指定znode, 如果第二个参数为true.

[zk: localhost:2181(CONNECTED) 35] create /hello world

WATCHER::

WatchedEvent state:SyncConnected type:NodeCreated path:/hello
Created /hello
[zk: localhost:2181(CONNECTED) 36] stat /hello true
cZxid = 0x262f0f0
ctime = Wed Mar 21 14:56:31 CST 2018
mZxid = 0x262f0f0
mtime = Wed Mar 21 14:56:31 CST 2018
pZxid = 0x262f0f0
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0