服务网格:微服务进入2.0时代

微服务自2014年3月由Martin Fowler首次提出以来,在Spring CloudDubbo等各类微服务框架的帮助下,以燎原之势席卷了整个IT技术界,成为了最主流的分布式应用解决方案。但仍然还有很多问题没有得到根本性的解决,比如技术门槛高、多语言支持不足、代码侵入性强等。如何应对这些挑战成为了下一代微服务首要回答的问题。直到服务网格(Service Mesh)被提出,这一切都有了答案。

1 微服务之殇

时光回到2017年初,那时所有主流的微服务框架,不管是类库性质的FinagleHystrix,还是框架性质的Spring Cloud、Dubbo,本质上都归于应用内解决方案,都存在以下三个问题:

  • 技术门槛高:随着微服务实施水平的不断深化,除了基础的服务发现配置中心授权管理之外,团队将不可避免的在服务治理层面面临各类新的挑战,包括但不限于分布式跟踪、熔断降级、灰度发布、故障切换等,这对团队提出了非常高的技术要求。

service-governance

图片出处:Service Mesh:下一代微服务

  • 多语言支持不足:对于稍具规模的团队,尤其在高速成长的互联网创业公司,多语言的技术栈是常态,跨语言的服务调用也是常态,但目前开源社区上并没有一套统一的、跨语言的微服务技术栈。
  • 代码侵入性强:主流的微服务框架(比如Spring Cloud、Dubbo)或多或少都对业务代码有一定的侵入性,框架替换成本高,导致业务团队配合意愿低,微服务落地困难。

这些问题加起来导致的结果就是,在实施微服务的过程中,小团队Hold不住,大公司推不动。

2 另辟蹊径

如何解决上述三个问题呢?最容易想到的是代理模式,在LB层(比如NginxApache HTTP Server)处理所有的服务调用,以及部分服务治理问题(比如分布式跟踪、熔断降级)。但这个方案有两个显著的缺点,第一,中心化架构,代理端自身的性能和可用性将是整个系统的瓶颈;第二,运维复杂度高,业务团队笑了,运维团队哭了。

难道这就是桃园吗?

服务网格(Service Mesh)应运而生!自2016年9月Linkerd第一次公开使用之后,伴随着LinkerdEnvoyIstioNGINX Application PlatformConduit等新框架如雨后春笋般不断涌现,在微服务之后,服务网格和它的边车(Sidecar)模式引领了IT技术界2017一整年的走向。

3 服务网格

3.1 元定义

首先,我们来看一下服务网格的提出者William Morgan是如何描述它的。

A service mesh is a dedicated infrastructure layer for handling service-to-service communication. Consists of a control plane and data plane (service proxies act as "mesh"). - William Morgan, What's a Service Mesh? And Why Do I Need One?

上面这段话非常清晰的指明了服务网格的职责,即处理服务间通讯,这正是服务治理的核心所在。而a dedicated infrastructure layer这几个单词将服务网格和之前所有的微服务框架(framework)划清了界限,也即服务网格独立于具体的服务而存在,这从根本上解决了前文提到的老的微服务框架在多语言支持和代码侵入方面存在的问题。并且,由于服务网格的独立性,业务团队不再需要操心服务治理相关的复杂度,全权交给服务网格处理即可。

那你可能会问,这不跟之前提到的代理模式差不多吗?区别在于服务网格独创的边车模式。针对每一个服务实例,服务网格都会在同一主机上一对一并行部署一个边车进程,接管该服务实例所有对外的网络通讯(参见下图)。这样就去除了代理模式下中心化架构的瓶颈。同时,借助于良好的框架封装,运维成本也可以得到有效的控制。

linkerd-service-mesh-diagram

图片出处:What's a Service Mesh? And Why Do I Need One?

3.2 演化史

追本溯源,服务网格从无到有可分为三个演化阶段(参见下图)。第一个阶段,每个服务各显神通,自行处理对外通讯。第二个阶段,所有服务使用统一的类库进行通讯。第三个阶段,服务不再关心通讯细节,统统交给边车进程,就像在TCP/IP协议中,应用层只需把要传输的内容告诉TCP层,由TCP层负责将所有内容原封不动的送达目的端,整个过程中应用层并不需要关心实际传输过程中的任何细节。

pattern-network

pattern-library

pattern-sidecar

图片出处:Pattern: Service Mesh

3.3 时间线

最后,再来回看一下服务网格年轻的历史。虽然服务网格的正式提出是在2016年9月,但其实早在2013年,Airbnb就提出了类似的想法——SmartStack,只不过SmartStack局限于服务发现,并没有引起太多关注,类似的还有Netflix的Prana和唯品会的OSP Local Proxy。2016年服务网格提出之后,以Linkerd和Envoy为代表的框架开始崭露头角,并于2017年先后加入CNCF基金(Cloud Native Computing Foundation),最终促使了一代新贵Istio的诞生。2018年,Istio将发布1.0版本,这也许意味着微服务开始进入2.0时代。

history

图片出处:Service Mesh:下一代微服务

4 参考

0

Common Pool2 对象池应用浅析

我们系统中一般都会存在很多可重用并长期使用的对象,比如线程、TCP 连接、数据库连接等。虽然我们可以简单的在使用这些对象时进行创建、使用结束后销毁,但初始化和销毁对象的操作会造成一些资源消耗。我们可以使用对象池将这些对象集中管理,减少对象初始化和销毁的次数以节约资源消耗。

顾名思义,对象池简单来说就是存放对象的池子,可以存放任何对象,并对这些对象进行管理。它的优点就是可以复用池中的对象,避免了分配内存和创建堆中对象的开销;避免了释放内存和销毁堆中对象的开销,进而减少垃圾收集器的负担;避免内存抖动,不必重复初始化对象状态。对于构造和销毁比较耗时的对象来说非常合适。

当然,我们可以自己去实现一个对象池,不过要实现的比较完善还是要花上不少精力的。所幸的是, Apache 提供了一个通用的对象池技术的实现: Common Pool2,可以很方便的实现自己需要的对象池。Jedis 的内部对象池就是基于 Common Pool2 实现的。

核心接口

Common Pool2 的核心部分比较简单,围绕着三个基础接口和相关的实现类来实现:

  • ObjectPool:对象池,持有对象并提供取/还等方法。
  • PooledObjectFactory:对象工厂,提供对象的创建、初始化、销毁等操作,由 Pool 调用。一般需要使用者自己实现这些操作。
  • PooledObject:池化对象,对池中对象的封装,封装对象的状态和一些其他信息。

Common Pool2 提供的最基本的实现就是由 Factory 创建对象并使用 PooledObject 封装对象放入 Pool 中。

对象池实现

对象池有两个基础的接口 ObjectPoolKeyedObjectPool, 持有的对象都是由 PooledObject 封装的池化对象。 KeyedObjectPool 的区别在于其是用键值对的方式维护对象。

ObjectPoolKeyedObjectPool 分别有一个默认的实现类 GenericObjectPoolGenericKeyedObjectPool 可以直接使用,他们的公共部分和配置被抽取到了 BaseGenericObjectPool 中。

SoftReferenceObjectPool 是一个比较特殊的实现,在这个对象池实现中,每个对象都会被包装到一个SoftReference中。SoftReference允许垃圾回收机制在需要释放内存时回收对象池中的对象,可以避免一些内存泄露的问题。

ObjectPool

下面简单介绍一下 ObjectPool 接口的核心方法,KeyedObjectPoolObjectPool 类似,区别在于方法多了个参数: K key

public interface ObjectPool<T> {

    // 从池中获取一个对象,客户端在使用完对象后必须使用 returnObject 方法返还获取的对象
    T borrowObject() throws Exception, NoSuchElementException,
            IllegalStateException;

    // 将对象返还到池中。对象必须是从 borrowObject 方法获取到的
    void returnObject(T obj) throws Exception;

    // 使池中的对象失效,当获取到的对象被确定无效时(由于异常或其他问题),应该调用该方法
    void invalidateObject(T obj) throws Exception;

    // 池中当前闲置的对象数量
    int getNumIdle();

    // 当前从池中借出的对象的数量
    int getNumActive();

    // 清除池中闲置的对象
    void clear() throws Exception, UnsupportedOperationException;

    // 关闭这个池,并释放与之相关的资源
    void close();

    ...
}

PooledObjectFactory

对象工厂,负责对象的创建、初始化、销毁和验证等工作。Factory 对象由ObjectPool持有并使用。

public interface PooledObjectFactory<T> {

    // 创建一个池对象
    PooledObject<T> makeObject() throws Exception;

    // 销毁对象
    void destroyObject(PooledObject<T> p) throws Exception;

    // 验证对象是否可用
    boolean validateObject(PooledObject<T> p);

    // 激活对象,从池中取对象时会调用此方法
    void activateObject(PooledObject<T> p) throws Exception;

    // 钝化对象,向池中返还对象时会调用此方法
    void passivateObject(PooledObject<T> p) throws Exception;
}

Common Pool2 并没有提供 PooledObjectFactory 可以直接使用的子类实现,因为对象的创建、初始化、销毁和验证的工作无法通用化,需要由使用方自己实现。不过它提供了一个抽象子类 BasePooledObjectFactory,实现自己的工厂时可以继承 BasePooledObjectFactory,就只需要实现 createwrap 两个方法了。

PooledObject

PooledObject 有两个实现类,DefaultPooledObject 是普通通用的实现,PooledSoftReference 使用 SoftReference 封装了对象,供 SoftReferenceObjectPool 使用。

下面是 PooledObject 接口的一些核心方法:

public interface PooledObject<T> extends Comparable<PooledObject<T>> {

    // 获取封装的对象
    T getObject();

    // 对象创建的时间
    long getCreateTime();

    // 对象上次处于活动状态的时间
    long getActiveTimeMillis();

    // 对象上次处于空闲状态的时间
    long getIdleTimeMillis();

    // 对象上次被借出的时间
    long getLastBorrowTime();

    // 对象上次返还的时间
    long getLastReturnTime();

    // 对象上次使用的时间
    long getLastUsedTime();

    // 将状态置为 PooledObjectState.INVALID
    void invalidate();

    // 更新 lastUseTime
    void use();

    // 获取对象状态
    PooledObjectState getState();

    // 将状态置为 PooledObjectState.ABANDONED
    void markAbandoned();

    // 将状态置为 PooledObjectState.RETURNING
    void markReturning();
}

对象池配置

对象池配置提供了对象池初始化所需要的参数,Common Pool2 中的基础配置类是 BaseObjectPoolConfig。其有两个实现类分别为 GenericObjectPoolConfigGenericKeyedObjectPoolConfig,分别为 GenericObjectPoolGenericKeyedObjectPool 所使用。

下面是一些重要的配置项:

  • lifo 连接池放池对象的方式,true:放在空闲队列最前面,false:放在空闲队列最后面,默认为 true
  • fairness 从池中获取/返还对象时是否使用公平锁机制,默认为 false
  • maxWaitMillis 获取资源的等待时间。blockWhenExhausted 为 true 时有效。-1 代表无时间限制,一直阻塞直到有可用的资源
  • minEvictableIdleTimeMillis 对象空闲的最小时间,达到此值后空闲对象将可能会被移除。-1 表示不移除;默认 30 分钟
  • softMinEvictableIdleTimeMillis 同上,额外的条件是池中至少保留有 minIdle 所指定的个数的对象
  • numTestsPerEvictionRun 资源回收线程执行一次回收操作,回收资源的数量。默认 3
  • evictionPolicyClassName 资源回收策略,默认值 org.apache.commons.pool2.impl.DefaultEvictionPolicy
  • testOnCreate 创建对象时是否调用 factory.validateObject 方法,默认 false
  • testOnBorrow 取对象时是否调用 factory.validateObject 方法,默认 false
  • testOnReturn 返还对象时是否调用 factory.validateObject 方法,默认 false
  • testWhileIdle 池中的闲置对象是否由逐出器验证。无法验证的对象将从池中删除销毁。默认 false
  • timeBetweenEvictionRunsMillis 回收资源线程的执行周期,默认 -1 表示不启用回收资源线程
  • blockWhenExhausted 资源耗尽时,是否阻塞等待获取资源,默认 true

池化对象的状态

池化对象的状态定义在 PooledObjectState 枚举中,有以下值:

  • IDLE 在池中,处于空闲状态
  • ALLOCATED 被使用中
  • EVICTION 正在被逐出器验证
  • VALIDATION 正在验证
  • INVALID 驱逐测试或验证失败并将被销毁
  • ABANDONED 对象被客户端拿出后,长时间未返回池中,或没有调用 use 方法,即被标记为抛弃的

这些状态的转换逻辑大致如下图:

状态流转图

Demo

最后,我们来实现一个简单的 Demo 来上手 Common Pool2 的使用,这是一个 StringBuffer 的对象池的使用。

首先要实现工厂的创建、封装和销毁操作。对象池和池化对象封装使用默认实现就可以了。

public class StringBufferFactory extends BasePooledObjectFactory<StringBuffer> {
    // 创建一个新的对象
    @Override
    public StringBuffer create() {
        return new StringBuffer();
    }

    // 封装为池化对象
    @Override
    public PooledObject<StringBuffer> wrap(StringBuffer buffer) {
        return new DefaultPooledObject<>(buffer);
    }

    // 使用完返还对象时将 StringBuffer 清空
    @Override
    public void passivateObject(PooledObject<StringBuffer> pooledObject) {
        pooledObject.getObject().setLength(0);
    }
}

然后就可以使用对象池了,基本的操作就是获取、返还和标记失效等。

// 创建对象池配置
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
// 创建对象工厂
PooledObjectFactory factory = new StringBufferFactory();
// 创建对象池
ObjectPool<StringBuffer> pool = new GenericObjectPool<>(factory, config);

StringReader in = new StringReader("abcdefg");

StringBuffer buf = null;
try {
    // 从池中获取对象
    buf = pool.borrowObject();

    // 使用对象
    for (int c = in.read(); c != -1; c = in.read()) {
        buf.append((char) c);
    }
    return buf.toString();
} catch (Exception e) {
    try {
        // 出现错误将对象置为失效
        pool.invalidateObject(buf);
        // 避免 invalidate 之后再 return 抛异常
        buf = null; 
    } catch (Exception ex) {
        // ignored
    }

    throw e;
} finally {
    try {
        in.close();
    } catch (Exception e) {
        // ignored
    }

    try {
        if (null != buf) {
            // 使用完后必须 returnObject
            pool.returnObject(buf);
        }
    } catch (Exception e) {
        // ignored
    }
}

总结

Common Pool2 的应用非常广泛,在日常的开发工作中也有很多使用场景。它的整体架构也并不复杂,可以将其简单划分为 3 个角色和相关的配置、状态,掌握起来比较简单。而且 Common Pool2 官方也提供了一些通用的实现,有特殊的开发需求时也可以简单的扩展其提供的抽象类,可以满足大部分的日常开发需求。

0

简单聊聊各种语言的函数扩展

背景

最近有同事反应,我们运营后台下载的 CSV 文件出现错乱的情况。问题的原因是原始数据中有 CSV 中非法的字符,比如说姓名字段,因为是用户填写的,内容有可能包含了 ," 等字符,会导致 CSV 文件内容错乱。

于是我就想用一个简单的方式来解决这个问题。一个简单粗暴的解决方案就是导出时对字符串进行处理,将一些特殊字符替换掉,或者前后用"包起来。但是这样的话,需要所有下载 CSV 的地方都要改写,会比较麻烦。如果我们可以简单的给 String 增加一个方法(如 String.csv())直接就把字符串处理成 CSV 兼容的格式,就会方便很多。我们的运营后台是使用 Scala 语言开发的,所幸的是,Scala 里提供了一个非常强大的功能,可以满足我们的需求,那就是隐式转换。

Scala 的隐式转换

在 Scala 里可以通过 implicit 隐式转换来实现函数扩展。

编译器在碰到类型不匹配或是调用一个不存在的方法的时候,会去搜索符合条件的隐式类型转换,如果找不到合适的隐式转换方法则会报错。

下面是处理 CSV 下载字符串的代码:

trait CsvHelper {
  implicit def stringToCsvString(s: String) = new CsvString(s)
}
class CsvString(val s: String){
  def csv = s"""${s.replaceAll(",", " ").replaceAll("\"", "'")}"""
}

class Controller extends CsvHelper {
    def dowload(){
        ...
        ",foo,".csv //foo
    }
}

Controller 中我调用 String.csv 方法,但是 String 没有 csv 方法。这时候编译器就会去找 Controller 中有没有隐式转换的方法,发现在其父类 CsvHelper 中有方法把 String 转换成 CsvString,而 CsvString 中实现了 csv 方法。所以编译器最终会调用到 CsvString.csv 这个方法。

隐式转换是一个很强大,但是也很容易误用的功能。Scala 里隐式转换有一些基本规则:

  • 优先规则:如果存在两个或者多个符合条件的隐式转换,如果编译器不能选择一条最优的隐式转换,则提示错误。具体的规则是:当前类中的隐式转换优先级大于父类中的隐式转换;多个隐式转换返回的类型有父子关系的时候,子类优先级大于父类。
  • 隐式转换只会隐式的调用一次,编译器不会调用多个隐式方法,不会产生调用链。
  • 如果当期代码已经是合法的,不需要隐式转换则不会使用隐式转换。

Java 的动态扩展

我们再来看看我们熟悉的 Java 语言。Java 是一门静态语言,本身没有直接提供动态扩展的方法,但是我们可以通过 AOP 动态代理的方式来修改一个方法,从而间接的实现方法的动态扩展。

下面就是一个我们就用 AspectJ 来实现一个动态扩展,用于分页查询后获取数据的总条数。

@Aspect
@Component
public class PaginationAspect {
    @AfterReturning(
        pointcut = "execution(* com.xingren..*.*ByPage(..))",
        returning = "result"
    )
    public void afterByPage(JoinPoint joinPoint, Object result) {
        //根据result获取sql信息,再查询总条数封装到result中。
    }
}

其中 AfterReturning 注解表明在被注解方法返回后的一些后续动作。pointcut 定义切点的表达式,可以用通配符 * 表示;returning 指定返回的参数名。然后就可以对返回的结果进行处理。这样就可以达到动态的修改原始函数功能。

当然除了 AspectJ 也可以使用 CGLib 来代理来实现简单的 AOP。

public class FooService {
    public Page findByPage(){
        return new Page();
    }
    public Page findPage(){
        return new Page();
    }
}
@Data
public class Page {
    private String sql = "";
    private List<Object> content = new ArrayList();
    private Integer size = 0;
    private Integer page = 0;
    private Integer total = 0;
}

创建一个对象 FooService 用来模拟查询分页方法。

public class CGLibProxyFactory implements MethodInterceptor {

    private Object object;

    public CGLibProxyFactory(Object object){
        this.object = object;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("before method! do something...");

        Object result = methodProxy.invoke(object, objects);
        //进行方法判断,是否需要处理
        if (method.getName().contains("ByPage")) {
            if (result instanceof Page) {
                System.out.println("after method! do something...");
                ((Page) result).setTotal(100);
            }
        }
        return result;
    }
}

创建一个代理类实现 MethodInterceptor 接口,手动调用 invoke 方法,用来动态的修改被代理的实现方法。可以在执行之前做一些参数校验,或者一些参数的预处理。也可以获取修改执行的结果,或者干脆不调用 invoke 方法,自定义实现。也可以在调用后做一些后续动作。

public class ObjectFactoryUtils {
    public static <T> Optional<T> getProxyObject(Class<T> clazz) {
        try {
            T obj = clazz.newInstance();
            CGLibProxyFactory factory = new CGLibProxyFactory(obj);
            Enhancer enhancer=new Enhancer();//利用`Enhancer`来创建被代理类的代理实例
            enhancer.setSuperclass(clazz);//设置目标class
            enhancer.setCallback(factory);//设置回调代理类
            return Optional.of((T)enhancer.create());
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return Optional.empty();
    }
}

public static void main(String[] args) {
        Optional<FooService> proxyObject = ObjectFactoryUtils.getProxyObject(FooService.class);
        if(proxyObject.isPresent()) {
            FooService foo = proxyObject.get();
            System.out.println("findByPage:");
            System.out.println(foo.findByPage().getTotal());
            System.out.println("findPage:");
            System.out.println(foo.findPage().getTotal());
        }
}

最后打印的输出是:

findByPage:
before method! do something...
after method! do something...
100
findPage:
before method! do something...
0

当然除了 CGLIB 代理也可以使用 Proxy 动态代理,同样的逻辑也可以达到动态的修改原始方法的目的,从而间接的实现函数扩展。不过 Proxy 动态代理是基于接口的代理。

其它语言的函数扩展

其实除了 Scala 的隐式转换和 Java 的动态代理,其他很多语言也能支持各种不同的函数扩展。

Swift

在 Swift 中可以通过关键词 extension 对已有的类进行扩展,可以扩展方法、属性、下标、构造器等等。

extension Int {
    func times(task: () -> Void) {
        for _ in 0..<self {
            task()
        } 
    }
}

比如说我给 Int 增加一个 times 方法。即执行任务的次数。就可以如下使用:

2.times({
    print("Hello!")
})

上面的代码会执行 2 次打印方法。

Go

在 Go 中可以通过在方法名前面加上一个变量,这个附加的参数会将该函数附加到这种类型上。即给一个方法加上接收器。

func (s string) toUpper() string {
    return strings.ToUpper(s)
}

"aaaaa".toUpper //输出 AAAAA

Kotlin

Kotlin 的函数扩展非常简单,就是定义的时候,函数名写成 接收器 + . + 方法名 就行了。

class C {

}
fun C.foo() { println("extension") }

C().foo() //输出extension

注意当给一个类扩展已有的方法的时候,默认使用的是类自带的成员函数。如下:

class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

C().foo() //输出member

可以通过函数重载的方式区分成员函数(fun C.foo(i:Int) { println("extension") }),在调用的地方显示的区分。

JavaScript

在 JavaScript 中也可以很方便的给一个对象扩展函数。写法就是 对象 + . + 函数名

var date = new Date();
date.format = function() {
    return this.toISOString().slice(0, 10);
}
date.format(); //"2017-11-29"

也可以给一个 Object 进行扩展:

Date.prototype.format = function() {
     return this.toISOString().slice(0, 10);
}
new Date().format(); //"2017-11-29"

总结

其实了解不同语言对于函数扩展的实现挺有意思的,本文只是粗略的介绍了一下。合理的使用这些语言的扩展,可以帮助我们提高代码质量和工作效率。我们还可以通过函数扩展来对第三方类库进行修改或者扩展,从而更灵活的调用第三方类库。

0