服务网关-apisix、envoy实践前期

1,330 阅读11分钟

初衷:

1.治理东西流量

eg:gPRC基于uid灰度,分流

2.grpc的服务注册、发现

eg:替代现有的zookeeper注册中心


背景:

为什么不选择客户端负载均衡方式?

使用gRPC客户端负载均衡器,该负载均衡器被嵌入到gRPC客户端库中。

这样,每个客户端微服务都可以执行自己的负载均衡。

但是,最终的客户非常脆弱,需要大量的自定义代码来提供任何形式的弹性,指标或日志记录,所有这些我们的变更都会需要业务系统配合。

ProxyClient Side优势客户端可以专心业务逻辑,无需感知后端

nginx

haproxy

AWS ELB

SmartStack

Finagle

proxygen 和 wangle

gRPC

linkerd

nghttp2

首先Linkerd与Envoy的对比,作者在issue里有介绍:

github.com/envoyproxy/…

linkerd是一个独立的、开源的RPC路由代理,建立在Netty和Finagle上。

linkerd提供了许多Finagle的特性,包括感知延迟的负载平衡、连接池、断路、重试预算、截止日期、跟踪、细粒度检测,以及用于请求级路由的流量路由层。

linkerd提供了一个可插拔的服务发现接口(对Consul和ZooKeeper,以及Marathon和Kubernetes api提供标准支持)。

linkerd的内存和CPU要求明显高于envoy的。

然而,它的基础技术是广泛的生产测试和广泛部署。

与Envoy不同的是,linkerd提供了一种极简的配置语言,并且明确地不支持热加载,而是依赖于动态供应和服务抽象。

linkerd支持HTTP/1.1, Thrift, ThriftMux, HTTP/2(实验性)和gRPC(实验性)。

至于Apisix和Envoy

网上一大堆,找出来的基本上都是相互类似的

APISIX 在响应延迟和 QPS 层面都略优于 Envoy, 由于 nginx 的多 worker 的协作方式在高并发场景下更有优势,得益于此, APISIX 在开启多个 worker 进程后性能提升较 Enovy 更为明显;

但是两者并不冲突, Envoy 的总线设计使它在处理东西向流量上有独特的优势, APISIX 在性能和延迟上的表现使它在处理南北向流量上具有海量的吞吐能力,根据自己的业务场景来选择合理的组件配合插件构建自己的服务才是正解。

基于雪球目前的openResty流量分发,envoy对应的功能是?

业内厂商实现案例:

网易轻舟:

其中有几个关注点:

1.轻舟的这套服务网格技术的研发成员有C++的研发人员

2.轻舟的envoy部署模式是sidecar模式(要是按照这种模式部署,对雪球来说步子跨的是不是有些大?)

从kong到envoy的演进:zhuanlan.zhihu.com/p/242260216…

其中有几个关注点:

1.从严选的架构可以看到在envoy之前,网易是有api网关规划的(当然雪球自研的openresty+warden有与之对应功能)

2.演进过程可以看出是兼容升级的(而我们目前旧的体系要实现哪些部分的梳理还欠缺)

HTTP2特性与GRPC协议实现

HTTP/2 Frequently Asked Questions

特性描述打个比方
单一连接Single Connection仅使用与服务器的一个连接来加载网站,并且只要该网站处于打开状态,该连接就保持打开状态。 这减少了建立多个TCP连接所需的往返次数。一个TCP连接上你就可以获取多个后端接口的数据
多路复用Multiplexing在同一连接上,同时允许多个请求。 以前,使用HTTP / 1.1,每次传输都必须等待其他传输完成。一个连接上每个的消息都是有streamId这个会话ID的,不需要等到其他的返回,也就不会阻塞
服务器推送Server Push可以将其他资源发送给客户端以供将来使用。简单理解就是服务端主动下发,不需要客户端轮训了
优先排序Prioritization为请求分配了依赖级别,服务器可以使用这些依赖级别来更快地交付更高优先级的资源。--
二进位Binary使HTTP / 2更易于服务器解析,更紧凑且更不易出错。 将信息从文本转换为二进制(计算机的本地语言)不会浪费任何额外的时间。之前的文本传输需要涉及到服务器和浏览器的处理,两边都浪费了时间
标头压缩Header CompressionHTTP / 2使用HPACK压缩,从而减少了开销。 在HTTP / 1.1中,每个请求中发送的许多标头都具有相同的值。好比在雪球的网页里面多个请求,其实header内容都差不多,没必要每次都传送给upstream,可以只传递不同项

grpc基于envoy治理实现

server端

这个使用envoy提供的ADS注册

路由规则和cluster、endpoint的注册则根据接口动态修改,这样整个设计模式就是按照service mesh概念里的控制面板抽离出来了

public static VirtualHost getVirtualHost(String name, String clusterName) {``    ``RouteAction routeAction = RouteAction.newBuilder()``            ``.setCluster(clusterName)``            ``.build();``    ``HeaderMatcher headerMatcher = HeaderMatcher.newBuilder()``            ``.setName(``"UID"``)``            ``.setExactMatch(``"5784024476"``)``            ``.build();``    ``RouteMatch routeMatch = RouteMatch.newBuilder()``            ``.setGrpc(GrpcRouteMatchOptions.newBuilder()) ``// 确定只匹配Grpc Match``            ``.setPrefix(``"/"``)``            ``.addHeaders(headerMatcher)``            ``.build();``    ``Route route = Route.newBuilder()``            ``.setMatch(routeMatch)``            ``.setRoute(routeAction)``            ``.build();``    ``VirtualHost virtualHost = VirtualHost.newBuilder()``            ``.setName(name)``            ``.addDomains(``"*"``)``            ``.addRoutes(route)``            ``.build();``    ``return virtualHost;``}

服务发现动态配置,示例代码:

Endpoint endpoint = ClusterSnapshot.toEndpoint(host + ``":" + port);``ClusterLoadAssignment clusterLoadAssignment = simpleCache.getSnapshot(``0``).endpoints().resources().get(CLUSTER_NAME);``LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.newBuilder()``        ``.addLbEndpoints(LbEndpoint.newBuilder().setEndpoint(endpoint).build())``        ``.build();``List<ClusterLoadAssignment> assignments = ImmutableList.<ClusterLoadAssignment>builder()``        ``.add(clusterLoadAssignment.toBuilder().addEndpoints(localityLbEndpoints).build())``        ``.build();``Cluster.EdsClusterConfig edsClusterConfig = ClusterSnapshot.getEdsClusterConfig(CLUSTER_NAME, DISCOVERY_CLUSTER_NAME);``List<Cluster> clusters = ImmutableList.<Cluster>builder()``        ``.add(ClusterSnapshot.getCluster(CLUSTER_NAME, edsClusterConfig))``        ``.build();``Snapshot snapshot = Snapshot.create(``        ``clusters,``        ``assignments,``        ``ImmutableList.of(),``        ``ImmutableList.of(),``        ``ImmutableList.of(),``        ``version);``simpleCache.setSnapshot(``0``, snapshot);

client端

对于路由信息目前是在header(也就是GRPC的类Metadata)里面附加路由参数:

skyao.io/learning-gr…

提供对读取和写入元数据数值的访问,元数据数值在调用期间交换。

key 容许关联到多个值。

这个类不是线程安全,实现应该保证 header 的读取和写入不在多个线程中并发发生。

client代码实现:ClientHeaderInterceptor

public class ClientHeaderInterceptor ``implements ClientInterceptor {``    ``private static final Logger logger = Logger.getLogger(ClientHeaderInterceptor.``class``.getName());``    ``Metadata.Key<String> cookie = Metadata.Key.of(``"cookie"``, Metadata.ASCII_STRING_MARSHALLER);``    ``@Override``    ``public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method,``                                                               ``CallOptions callOptions, Channel next) {``        ``return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {``            ``@Override``            ``public void start(Listener<RespT> responseListener, Metadata headers) {``                ``//此处为你登录后获得的cookie的值``                ``headers.put(cookie, ``"U=6371181803"``);``                ``super``.start(``new ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>(responseListener) {``                    ``@Override``                    ``public void onHeaders(Metadata headers) {``                        ``/**``                         ``* if you don't need receive header from server, you can``                         ``* use {@link io.grpc.stub.MetadataUtils#attachHeaders}``                         ``* directly to send header``                         ``*/``                        ``logger.info(``"header received from server:" + headers);``                        ``super``.onHeaders(headers);``                    ``}``                ``}, headers);``            ``}``        ``};``    ``}``}

调用,这部分先写死,后续更改为动态配置,示例代码:

static final Metadata.Key<String> COOKIE = Metadata.Key.of(``"cookie"``, Metadata.ASCII_STRING_MARSHALLER);``private final ManagedChannel originalChannel;``private final XUserDeviceServiceGrpc.XUserDeviceServiceBlockingStub blockingStub;``private final XUserDeviceServiceGrpc.XUserDeviceServiceBlockingStub headerBlockingStub;``public XueqiuPushUserClientTest(String host, ``int port) {``    ``originalChannel = ManagedChannelBuilder.forAddress(host, port).usePlaintext(``true``).build();``    ``//将ClientHeaderInterceptor和原有的channel结合,生成包含拦截器的channel``    ``Channel channel = ClientInterceptors.intercept(originalChannel, ``new ClientHeaderInterceptor());``    ``blockingStub = XUserDeviceServiceGrpc.newBlockingStub(channel);``    ``//如果只需要在客户端传送header,而不需要接受服务端的header可以简单调用MetadataUtils.attachHeaders注册meta数据而不用定义Interceptor``    ``XUserDeviceServiceGrpc.XUserDeviceServiceBlockingStub stub = XUserDeviceServiceGrpc.newBlockingStub(originalChannel);``    ``Metadata meta = ``new Metadata();``    ``meta.put(COOKIE, ``"U=6371181803"``);``    ``headerBlockingStub = MetadataUtils.attachHeaders(stub, meta);``}

 

JWT权限认证网关

雪球使用JWT的好处:

1.UC服务挂掉,其他调用UC业务暂不受影响

2.UC的调用频率降低,减少UC服务压力

3.调用UC服务的一方,不如行情减少了UC业务代码耦合的依赖,增加了风险的容错能力

X509

X.509是常见通用的证书格式。所有的证书都符合为Public Key Infrastructure (PKI) 制定的 ITU-T X509 国际标准。X.509是国际电信联盟-电信(ITU-T)部分标准和国际标准化组织(ISO)的证书格式标准。作为ITU-ISO目录服务系列标准的一部分,X.509是定义了公钥证书结构的基本标准。1988年首次发布,1993年和1996年两次修订。当前使用的版本是X.509 V3,它加入了扩展字段支持,这极大地增进了证书的灵活性。X.509 V3证书包括一组按预定义顺序排列的强制字段,还有可选扩展字段,即使在强制字段中,X.509证书也允许很大的灵活性,因为它为大多数字段提供了多种编码方案.

JWT 最常见的几种签名算法:HS256(HMAC-SHA256) 、RS256(RSA-SHA256) 还有 ES256(ECDSA-SHA256)。

这三种算法都是一种消息签名算法,得到的都只是一段无法还原的签名。区别在于消息签名与签名验证需要的 「key」不同。

  1. HS256 使用同一个「secret_key」进行签名与验证。一旦 secret_key 泄漏,就毫无安全性可言了。

    • 因此 HS256 只适合集中式认证,签名和验证都必须由可信方进行。
  2. RS256 是使用 RSA 私钥进行签名,使用 RSA 公钥进行验证。公钥即使泄漏也毫无影响,只要确保私钥安全就行。

    • RS256 可以将验证委托给其他应用,只要将公钥给他们就行。
  3. ES256 和 RS256 一样,都使用私钥签名,公钥验证。算法速度上差距也不大,但是它的签名长度相对短很多(省流量),并且算法强度和 RS256 差不多。

对于单体应用而言,HS256 和 RS256 的安全性没有多大差别。
而对于需要进行多方验证的微服务架构而言,显然 RS256/ES256 安全性更高。
只有 user 微服务需要用 RSA 私钥生成 JWT,其他微服务使用公钥即可进行签名验证,私钥得到了更好的保护。

使用 OpenSSL 生成 RSA/ECC 公私钥

RS256 使用 RSA 算法进行签名,可通过如下命令生成 RSA 密钥:

# 1. 生成 2048 位(不是 256 位)的 RSA 密钥

openssl genrsa -out rsa-private-key.pem 2048

# 2. 通过密钥生成公钥

openssl rsa -in rsa-private-key.pem -pubout -out rsa-public-key.pem

ES256 使用 ECDSA 算法进行签名,该算法使用 ECC 密钥,生成命令如下:

# 1. 生成 ec 算法的私钥,使用 prime256v1 算法,密钥长度 256 位。(强度大于 2048 位的 RSA 密钥)
openssl ecparam -genkey -name prime256v1 -out ecc-private-key.pem
# 2. 通过密钥生成公钥
openssl ec -in ecc-private-key.pem -pubout -out ecc-public-key.pem

更进一步,「JWT 生成」和「JWT 公钥分发」都可以直接委托给第三方的通用工具,比如 hydra

甚至「JWT 验证」也可以委托给「API 网关」来处理,应用自身可以把认证鉴权完全委托给外部的平台,而应用自身只需要专注于业务。这也是目前的发展趋势。

JWT解释:

JSON Web Token Introduction - jwt.io

使用jwtIO调试工具:

JSON Web Tokens - jwt.io

使用jjwt:

GitHub - jwtk/jjwt: Java JWT: JSON Web Token for Java and Android

spring-cloud-gateway

目前只做南北流量的处理

API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。

API网关封装了系统内部架构,为每个客户端提供一个定制的API。

它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。

API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。

通常,网关也是提供REST/HTTP的访问API。

采用spring-cloud方案:GitHub - singgel/SpringCloud-Templates: eureka和consul服务注册中心, feign集成restTemplate和ribbon, ribbon负载均衡, hystrix-dashboard熔断器, zuul-filter路由过滤, bus消息总线

spring-cloud-gateway模块

本地模拟成功,核心代码:

@Override``public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {``    ``//1. 获取请求``    ``ServerHttpRequest request = exchange.getRequest();``    ``//2. 则获取响应``    ``ServerHttpResponse response = exchange.getResponse();``    ``//3. 如果是登录请求则放行``    ``if (request.getURI().getPath().contains(``"/login"``)) {``        ``return chain.filter(exchange);``    ``}``    ``//4. 获取请求头``    ``HttpHeaders headers = request.getHeaders();``    ``//5. 请求头中获取令牌``    ``String token = headers.getFirst(AUTHORIZE_TOKEN);``    ``//6. 判断请求头中是否有令牌``    ``if (StringUtils.isEmpty(token)) {``        ``//7. 响应中放入返回的状态吗, 没有权限访问``        ``response.setStatusCode(HttpStatus.UNAUTHORIZED);``        ``//8. 返回``        ``return response.setComplete();``    ``}``    ``//9. 如果请求头中有令牌则解析令牌``    ``try {``        ``JwtUtil.parseJWT(token);``    ``} ``catch (Exception e) {``        ``e.printStackTrace();``        ``//10. 解析jwt令牌出错, 说明令牌过期或者伪造等不合法情况出现``        ``response.setStatusCode(HttpStatus.UNAUTHORIZED);``        ``//11. 返回``        ``return response.setComplete();``    ``}``    ``//12. 放行``    ``return chain.filter(exchange);``}

APISIX-JWT

摘自温铭:

一个微服务 API 网关具备了多项网关功能,就可以让用户的服务只关心业务本身,而和业务实现无关的功能,比如服务发现、服务熔断、身份认证、限流限速、统计、性能分析等,就可以在独立的网关层面来解决。

从这个角度来看,API 网关既可以替代 Nginx 的所有功能,来处理南北向的流量,也可以完成 Istio 控制面和 Envoy 数据面的角色,来处理东西向的流量。