Consul 与 K8S 滚动部署

不停机发布讨论

近期公司的运维同事对一些系统尝试利用 K8S 的滚动发布机制不停机发布系统,应用和系统的状况是:POD 里的应用实例是注册在 Consul 上,Consul 的健康检查间隔是 10 秒;它们配置的 K8S 滚动发布机制直接把跑应用的 POD 终止了。

这样出现了:应用实例被终止后到 Consul 下次健康检查之前,Consul 认为应用实例是还存活的,把请求分发给了已经被停止的实例,这些请求当然是失败的。有的系统因为下游不支持重试,之前出现应用重试导致出现异常数据,就把 Ribbon、Feign、Hystrix 等的重试机制禁用了。而没有了重试机制请求就直接失败了,这样用户就会感知到系统发布。

大家开会讨论怎么才能让系统发布不会影响请求响应:

  1. 另一个部门的同事说他们就是所有接口都支持重试,所以允许对服务的某个实例请求失败可以重试到另一个实例,这样基本不会影响请求处理。让所有接口支持重试是需要上下游系统协调的,而且需要比较长的时间来实现。还有个问题是,不是所有接口都能重试的,举个行业特定的例子:信贷审批一般都会去查借款人的人行征信报告,多查一次可能导致借款人几个月都没法借款,设想一个报告查询的请求发出去了、在响应还没落地之前,应用实例就被终止了,这肯定是没法接受的。所以应用实例需要在被终止之前得到一个机会—-可以优雅停机的机会,它需要一个停机的通知。对于前面的问题,就是 K8S 要在停机前通知到 POD 里的实例,等通知返回后再终止 POD。

  2. 这就带来这个小插曲,我提出 K8S 在停止 POD 之前给 POD 里的应用发个通知的时候,运维同事说 Consul 和 K8S 是不同的系统,没法做到,他们说的非常肯定。。。

K8S POD 生命周期管理

对于一个问题,自己解决不了 跟 问题无解 是有区别。

在网上搜索一番后找到了 POD 生命周期钩子

继续阅读

《从0开始学架构》–笔记

第2篇 架构是什么

系统 泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体(能力)。

从逻辑的角度拆分系统后,得到的单元就是“模块”,从物理的角度拆分系统后,得到的单元就是“组件”。划分模块的主要目的是职责分离,划分组件的主要目的是单元复用。

框架是组件规范,提供基础功能的产品。软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及这些结构的描述。框架关注的是“规范”,架构关注的是“结构”。

软件架构指软件系统的顶层结构:
* 架构需要明确系统包含哪些“个体”,个体可以是 子系统、模块、组件等。
* 架构需要明确个体的运作和协作的规则。
* 顶层结构可以更好地区分系统和子系统。

自话:软件架构确定了系统中应该包含哪些个体,以及个体之间应该如何协作,以提供某种能力,从而实现系统的价值。

第2篇 架构设计的历史背景

“模块”“对象”“组件”本质上都是对达到一定规模的软件进行拆分,区别只是在于随着软件的复杂度不断增加,拆分的粒度越来越粗,拆分的层次越来越高。

第3篇 架构设计的目的

架构设计的主要目的是为了解决软件系统复杂度带来的问题。

架构设计首先要分析系统的复杂度所在,然后针对这些复杂度进行设计、制定方案。

第4篇 复杂度来源:高性能

软件系统中高性能带来的复杂度主要体现在两方面,一方面是单台计算机内部为了提高性能带来的复杂度;另一方面是多态计算机集群为了高性能带来的复杂度。

第5篇 复杂度来源:高可用

系统的高可用本质都是通过“冗余”来实现的,方法是增加机器。

高性能增加机器的目的在于“提升”处理性能,高可用增加机器的目的在于“冗余”处理单元。

存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。

高可用状态决策:无论是计算高可用还是存储高可用,其基础都是“状态决策”,即系统能够判断当前状态是正常还是异常,如果出现异常就要采取行动来保证高可用。

决策方式:
* 独裁式:所有冗余的个体向一个独立的决策主体上报状态信息,决策者进行决策。问题在于决策者本身是个单点。
* 协商式:两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策。难点在于两者的信息交换出现问题时如何决策。
* 民主式:指多个独立的个体通过投票的方式来进行决策。缺陷是脑裂:原来统一的集群因为连接中断,造成两个独立分隔的子集群,每个子集群单独进行选举,选出两个决策者。解决方法是要求“投票节点数必须超过系统总结点数一半”。

第6篇 复杂度来源:可扩展性

可扩展性是指为了应对将来需求变化而提供的一种扩展能力。

设计具备良好可扩展性的系统,有两个基本条件:正确预测变化、完美封装变化。

预测变化的复杂性在于:不能每个设计点都考虑可扩展性、不能完全不考虑可扩展性、所有的预测都存在出错的可能性。

设计具备良好可扩展性的系统,有两个思考角度:从业务维度,对业务深入理解,对可预计的业务变化进行预测;从技术维度,利用扩展性好的技术,实现对变化的封装。

应对变化的常见方案一是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。
方案二是提炼出一个“抽象层”和一个“实现层”,抽象是文档的,实现可根据具体业务需要定制开发,加入新的功能时,只需要增加新的实现,无需修改抽象层。

举例:设计一个支付网关对接不同的支付机构,为业务系统提供支付能力。每家支付机构的通信方式、请求/响应格式都是不一样的,但基本参数都是四要素信息(银行卡号、预留手机号、姓名、身份证号)、扣款金额等,扣款结果一般就是成功/失败/等通知,因此可以抽象出统一的接口,对接不同支付机构的具体实现类实现这个接口、完成具体的调用逻辑,当要对接新的支付机构时只需要添加一个实现类;业务系统只需访问这个统一的接口。

继续阅读

RxJava 线程模型

本文基于 RxJava 2.1.2 。根据代码和输出日志会更容易理解。

RxJava 的线程模型如下:

1. 不指定线程的情况

  • 不指定线程也就是不使用 observeOnsubscribeOn,所有操作在调用 subscribe 的线程执行。
@Test
public void noThread() {
    buildObservable().subscribe();
}

上面代码的输出为:

Thread[main]   execute   Action start emmit
Thread[main]   execute   Operation-1, event: 1
Thread[main]   execute   Operation-2, event: 1

2. subscribeOn

  • subscribeOn 不管调用多少次,只以第一次为准。如果只使用了 subscribeOn、没有使用 observeOn,则所有操作在第一次调用生成的线程里执行。
@Test
public void subscribeOn() throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(1);

    Observable<Integer> observable = buildObservable();
    observable
        .subscribeOn(scheduler("subscribeOn-1"))
        .subscribeOn(scheduler("subscribeOn-2"))
        .subscribe(i -> {
            showMessageWithThreadName("Action subscribe");
            latch.countDown();
        });

    latch.await();
}

上面代码的输出为:

create scheduler subscribeOn-2
create scheduler subscribeOn-1
Thread[subscribeOn-1]   execute   Action start emmit
Thread[subscribeOn-1]   execute   Operation-1, event: 1
Thread[subscribeOn-1]   execute   Operation-2, event: 1
Thread[subscribeOn-1]   execute   Action subscribe

3. observeOn

  • observeOn 必须跟 subscribeOn 一起使用,单独使用会抛出空引用异常。
  • observeOn 应在 subscribeOn 的后面调用,否则会出现死锁的情况。
  • observeOn 操作会更改后续操作的执行线程,直至下一个 observeOn 调用之前的操作或 subscribe 操作。

继续阅读

RxJava

ReactiveX

ReactiveX 是一个用于异步编程的 API 规范。 ReactiveX 结合了 Observer 模式、Iterator 模式和函数式编程的最佳理念。

ReactiveX 带来了更好的代码基础:

  • Functional, 函数式:避免了复杂的有状态的程序,在可观察流上使用干净(无副作用)的 输入/输出 函数。
  • Less is more, 少即是多:ReactiveX 的操作子通常把精心制作的修改简化为几行代码。
  • Async error handling, 异步错误处理:传统的 try/catch 对于异步计算的错误非常乏力,但 ReactiveX 具有恰当的机制来处理错误。
  • Concurrency made easy, 更容易的并发:ReactiveX 的 Observables 和 Schedulers 允许程序员从底层的线程、同步和并发问题中抽象出来。

RxJava

RxJava 是 ReactiveX 在 Java 编程语言里的一个实现。

基本概念:

  • 事件:主题生成的、订阅者感兴趣的东西。
  • 订阅者:Observer,抽象基类是 Subscriber
  • 主题:被观察的对象,抽象基类是 Observable。每个主题都有一个 OnSubscribe 的实例,OnSubscribe 从类名看是对订阅行为的反应,其 call(Subscriber subscriber) 方法封装了事件发生、通知的逻辑,供每次订阅时调用。
  • 订阅:subscribe,是一种动作,RxJava 在订阅时建立主题与监听者的关系,每次订阅,主题都会调用其内部 OnSubscribe.call(Subscriber subscriber) 方法。

  • 对于 Observable.doOnNext/doOnCompleted/doOnError/doOnEach/map 这类中间操作,生成一个新的订阅者 Subscriber,封装了相关行为,用于添加新的逻辑,并代理了对之前订阅者的调用;用新的订阅者和当前主题创建新的主题并返回。(采用的是包装器模式)
    继续阅读

应用事务管理混乱导致的一个坑

Spring 的事务传播属性

org.springframework.transaction.annotation.Propagation 定义了 Spring 的事务传播属性:

  • REQUIRED: 支持当前事务,如果不存在则新建一个。

  • REQUIRES_NEW: 创建新的事务,如果当前存在一个则 suppend 当前的。

  • SUPPORTS: 支持当前事务,如果不存在则以非事务方式执行。

  • MANDATORY: 支持当前事务,如果不存在则抛出异常。

  • NOT_SUPPORTED: 以非事务方式执行,如果当前存在一个事务则 suppend 当前的。

  • NEVER: 以非事务方式执行,如果存在事务则抛出异常。

  • NESTED: 如果当前存在一个事务则以嵌套事务的方式执行。

一个生产问题

一开始是 DBA 反馈数据库出现两种现象:

  1. 出现一些操作做完但会话一直还在等待客户端的提交动作。
  2. 偶尔出现大量的行锁,导致 JVM 线程互相等待而假死。

有个获取流水号的方法 systemService.getSerialNo 的事务传播属性是 NOT_SUPPORTED 的,这个方法通过类似这样的 update t_serialno set serial_no = :newSerialNo where serial_key = :key and serial_no = :oldSerialNo SQL 语句进行更新,更新返回的受影响行数等于 1 认为新的流水号是不重复的,更新不成功则重试。

行锁就出现在这些流水号的更新上。

继续阅读

微热山丘,探索 IoC、AOP 实现原理(二) AOP 实现原理

AOP 实现原理

一、简介

AOP 是 Aspect Oriented Programming 的简写,面向切面编程。主要的作用是以一种统一的方式对程序的逻辑进行修改、增强处理。可以在编译时也可以在运行时实现。编译时处理一般是通过字节码处理技术,运行时进行的一般是通过动态代理技术实现。

二、AOP 核心概念

  • Concerns, 关注:有两类,核心关注–是关于业务逻辑的;横切关注–是一些通用的逻辑,比如日志、缓存。
  • Joinpoint, 连接点:是执行时的切入点。可以是字段访问也可以是被调用方法。基于动态代理技术实现的一般只支持方法调用的连接点。
  • Target, 目标:是一个被切入的地方,一般是一个业务逻辑。比如是对一个业务逻辑的方法的调用。
  • Pointcut, 切入点:并不是所有的连接点都需要切入,切入点用于指定哪些连接点需要切入。
  • Advice, 建议:定义了 Aspect 的任务和什么时候执行它,是在核心关注之前还是之后。
  • Aspect, 方面:Advice 和 pointcut 定义一个方面。Advice 定义 Aspect 的任务和什么时候执行它,而切入点 pointcut 定义在哪里具体地方切入。就是说 Aspect 定义了它是什么东西、什么时候切入和在哪里切入。
  • Weaving, 织入:织入是一个把横切方面混合到业务目标对象的过程。可以是在编译时间,也可以是在运行时使用类加载机制,Spring AOP 是在运行时生成 bean 时处理。

下面是在 Sping 的 XML 里定义一个 AOP 的配置,注意其中个元素间的关系:

<!-- 一个 weaving 的定义 -->
<aop:config>
    <!-- 定义 aspect, ref 指向 任务的定义 -->
    <aop:aspect id="aspectCommonLogHandler" ref="commonLogHandler">
        <!-- 定义 pointcut -->
        <aop:pointcut id="commonLogPointcut" expression="execution( * net.coderbee.*.controller..*.*(..))" />

        <!-- 定义 advice, around 表示在目标的前/后执行 -->
        <aop:around method="inceptor" pointcut-ref="commonLogPointcut" />
    </aop:aspect>
</aop:config>

三、AOP 实现

1. 各组件定义配置

warnhill 的 AOP 实现没有采用 Spring 那样的配置方式,而是采用 bean 定义的形式组织起来:

<!-- 定义织入逻辑的实现 -->
<bean id="aspectJAutoProxyCreator" class="net.coderbee.warmhill.aop.AspectJAutoProxyCreator" />

<!-- 定义 pointcut -->
<bean id="pointcut" class="net.coderbee.warmhill.aop.AspectJExpressionPointcut" >
    <property name="expression" value="execution(* net.coderbee..*.*(..))" />
</bean>

<!-- 定义 任务 -->
<bean id="timerInterceptor" class="net.coderbee.warmhill.aop.TimerInterceptor" />
<bean id="anotherInterceptor" class="net.coderbee.warmhill.aop.AnotherInterceptor" />

<!-- 定义 aspect -->
<bean id="testAdvisor" class="net.coderbee.warmhill.aop.AspectJExpressionPointcutAdvisor">
    <property name="advice" ref="timerInterceptor" />
    <property name="pointcut" ref="pointcut" />
</bean>
<bean id="testAdvisor2" class="net.coderbee.warmhill.aop.AspectJExpressionPointcutAdvisor">
    <property name="advice" ref="anotherInterceptor" />
    <property name="pointcut" ref="pointcut" />
</bean>

继续阅读

微热山丘,探索 IoC、AOP 实现原理(一) IoC 实现原理

一. 简介及项目设定

1.1 微热山丘 介绍

warmhill(微热山丘)是一个参考 Spring 实现 IoC、AOP 特性的小项目。

相比于 Spring 庞杂的分类、层层继承、抽象,warmhill(微热山丘) 里都是简单直接的类、方法调用,核心在于简洁地展现实现原理。

1.2 项目设定及限制

有如下的限定:
1. 所有的 bean 都是单例。
2. bean 只有一个唯一的标识符 id,没有名字、别名。
3. 所有的 bean 都是立即初始化的,不支持延迟初始化。
4. 对于 BeanPostProcessor 的应用是基于声明的先后顺序。
5. 对于 AOP 的切入点,只支持对方法调用的拦截,不细分 Before/After/Around/Throw 等。
6. 对于 AOP 的配置也是基于 bean 定义的,不支持 <aop:config> 标签。
7. 目前只支持从 XML 方式配置 bean 。
8. bean 的属性目前只支持 String 类型和对其他 bean 的引用,只支持 setter 方法的依赖注入。

二. IoC、AOP 基本概念介绍

  • Resource: 资源。可存放在任意位置,只有一个方法: InputStream getStream();

  • BeanDefinition: bean 的定义信息。

  • BeanDefitionLoader: bean 定义加载类,只有一个方法: List<BeanDefinition> load();

  • BeanFactory: bean 工厂,负责根据 bean 定义创建 bean 的实例。

继续阅读

Spring 事务管理的一个 trick

问题

最近有同事碰到这个异常信息: Transaction rolled back because it has been marked as rollback-only ,异常栈被吃了,没打印出来。

调用代码大概如下:

@Component
public class InnerService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Throwable.class)
    public void innerTx(boolean ex) {
        jdbcTemplate.execute("insert into t_user(uname, age) values('liuwhb', 31)");
        if (ex) {
            throw new NullPointerException();
        }
    }

}

@Component
public class OutterService {
    @Autowired
    private InnerService innerService;

    @Transactional(rollbackFor = Throwable.class)
    public void outTx(boolean ex) {
        try {
            innerService.innerTx(ex);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

outterService.outTx(true);

他期望的是 innerService.innerTx(ex); 调用即使失败了也不会影响 OutterService.outTx 方法上的事务,只回滚了 innerTx 的操作。

结果没有得到他想要的,调用 OutterService.outTx 的外围方法捕获到了异常,异常信息是 Transaction rolled back because it has been marked as rollback-onlyoutTx 的其他操作也没有提交事务。

分析

上述方法的事务传播机制的默认的,也就是 Propagation.REQUIRED,如果当前已有事务就加入当前事务,没有就新建一个事务。

事务性的方法 outTx 调用了另一个事务性的方法 innerTx 。调用方对被调用的事务方法进行异常捕获,目的是希望被调用方的异常不会影响调用方的事务。

但还是会影响调用方的行为的。Spring 捕获到被调用事务方法的异常后,会把事务标记为 read-only,然后调用方提交事务的时候发现事务是只读的,就会抛出上面的异常。

继续阅读

Feign

简介

Feign 是一个 Java 到 HTTP 客户端的粘合剂。Feign 的目标是以最少的开销和代码把 你的代码连接到 http api 上。通过定制的编解码器和错误处理,你可以请求任何基于文本的 http api 。

原理

通过处理注解信息来生成模板化请求。在发出请求前,参数直接应用到这些模板上。这限制了 Feign 只支持基于文本的 api,这显著简化了系统的一些方面如重放请求。

为什么选择 Feign

  • 依赖问题。目前项目组用的是 Hessian 做远程调用,由于 Hessian 存在对 jar 包的依赖,特别是一些项目升级到 JDK 1.8,采用 Maven 构建;而老的一些任然采用 1.6 ,是个简单的 Eclipse 工程,导致打包、部署非常麻烦。

  • 依赖于接口,使用简洁。对于客户端,只依赖于接口类,通过 Spring 注入实现,迁移基本不需要很大的改动。

  • 与 Spring Cloud 集成。Feign 本身是 Spring Cloud 的一个组件,可以通过 Ribbon 做路由,可以进一步提升服务化。

  • 系统对性能的要求并不是那种很严苛的,基于文本的调用也方便调试。

继续阅读

分布式系统间请求跟踪

一、请求跟踪基本原理

现在的很多应用都是由很多系统系统在支持,这些系统还会部署很多个实例,用户的一个请求可能在多个系统的部署实例之间流转。为了跟踪一个请求的完整处理过程,我们可以给请求分配一个唯一的 ID traceID,当请求调用到另一个系统时,我们传递这个 traceID。在输出日志时,把这个 traceID 也输出到日志里,这样,根据日志文件,提取出现这个 traceID 的日志就可以分析这个请求的完整调用过程,甚至进行性能分析。

当然,在一个系统内部,我们不希望每次调用一个方法时都要传递这个 traceID,因此在 Java 里,一般把这个 traceID 放到某种形式的 ThreadLocal 变量里。

日志类库在输出日志时,就从这个 ThreadLocal 变量里取出 traceID,跟要输出的日志信息一起写入日志文件。

这样对于应用的开发者来说,基本不需要关注这个 traceID

二、远程调用间传递跟踪信息

如何使用的是自定义的 RPC 实现,这些 RPC 一般都预留了扩展机制来传递一些自定义的信息,我们可以把 traceID 作为自定义信息进行传递。

对于 Hessian 这种基于 HTTP 协议的 RPC 方法,它把序列化后的调用信息作为 POST 的请求体。如果我们不想去修改 Hessian 的调用机制,可以把 traceID 放到 HTTP 的请求头里。

在客户端只需要提供封装好的 RPC 调用代理。

在服务端通过 Filter 得到 traceID 放入 ThreadLocal 变量。

继续阅读