瓜农老梁

一个想分享点干货的家伙,微信公众号「瓜农老梁」

0%

引言

越来越多的公司开始研究Service Mesh,线上大批量应用的依旧很少,已经上线的很多问题解决的并不完美,为后面迭代和稳定性埋下隐患。目前来看整体开源生态成熟度还有需要完善,本文为笔者试水service mesh过程中发现的问题归纳整理。

一、目标与价值

业务侧只需要引入轻量级SDK,其他基础能力下沉到网格SideCar代理中,一个美好的愿望 “接管所有非业务关心的能力”。

1.业务赋能价值

  • 提升开发效率:只需专注业务
  • 加速业务探索:快速迭代上线、快速验证
  • 代理升级无感知:不需要费力推动业务升级或者通过卡点升级引起的各类问题

2.运维提效价值

  • 治理体系统一:屏蔽不同语言体系治理的复杂性
  • 技术演进统一:不必关心版本碎片化问题,能力统一自住演进

二、组织形态整合

如果将Service Mesh作为公司战略推动,Service Mesh依赖Kubernate底座,相关人员最好整合到一个部门,统一运维和开发。

  • 将Service Mesh团队、Serverless团队、容器团队整合到一个部门负责云原生体系建设
  • 其他部门配合改造和对接
阅读全文 »

前言

在异地多活项目整体推过程中的一些注意事项和设计点归纳和整理,抛砖引玉,其中一些点还有待深入探讨和优化。

一、指导事项归纳

1.多活原因归纳

推动多活的原因大体可归纳为以下三种。

  • 高可用架构部署
  • 业务整体的容灾
  • 单机房容量限制

2.多活指导归纳

多活牵扯公司业务方方面面,整体来讲业务改造和基础设施中间件改造两大块。

  • 核心链路自包含可逻辑分片
  • 调用尽可能收敛在本单元
  • 流量分片逻辑尽可能均衡
  • 中间件多活架构改造升级
  • 业务改造支持多活方案
  • 业务场景验证中间件能力

3.推动事项归纳

顺利推进多活事项是公司重要战略,需要统一思想,将多活项目当成最高优先级推进。

  • 统一思想认识自觉对齐到公司级战略项目

  • 设置总架构师级别建议对齐部门负责人,对整体架构方案和结果负责

    例如:总架构师拥有对各个部门牵头同学拥有不低于60%的绩效考核权

  • 部门负责人作为该部门领导需要全力推动

  • 每个业务线设置接口人并负责该业务线所有对接和推动事务,对本业务线或者部门的推动结果负责

    例如:业务线接口人拥本业务参与多活事项同学不低于60%的绩效考核权

  • 项目架构师与各业务负责人周会例会及时跟进问题和进度

  • 各个牵头人梳理的问题对外沟通前,先部门内部对齐,提升沟通效率

4.抓核心链路

先保证核心链路的多活,避免面面俱到严重拖累进度,例如:

  • 优惠券库存类扣减先中心机房统一扣减
  • 管理运营类等无实时要求的先不做多活
  • 流量切换过程中容忍分钟级不可用,切换结束后恢复

二、多活规则与流量选择

1.路由因子选择与映射

路由因子选择: 需要根据公司业务场景选择,常见的路由因子有地域、用户ID。

路由因子与机房映射:

地域因子:将地域编号与机房建立映射,例如:001->unit-a

用户因子:将UID与机房建立映射,例如:123456与机房编号哈希后映射到unit-a

2.请求分配正确机房

一个请求有了多活规则后如何将请求路由到正确机房,归纳了以下几种方式:

  • 终端服务通过多域名切换:将请求直接路由到正确机房
  • 在反向代理层转发:转发属于异地机房流量
  • 在网关层转发:转发属于异地机房流量

3.多活管控中心服务

  • 多活部署通过双向同步或者双写方式保证数据的一致性
  • 提供SDK和服务接口供中间件或者服务服务映射规则
  • 提供流量切换的整个闭环流程

三、RPC跨机房调用能力

1.注册中心架构图

  • 节点注册时需要将机房信息一并注册
  • 注册中心提供跨机房双向同步能力

2.RPC框架跨机房调用

  • 默认本机房调用策略
  • 提供自定义路由功能供业务选择是否跨机房调用
  • 需要注意新老版本以及发布时是否存在流量倾斜问题

阅读全文 »

引言

本文对流量录制和回放常见的方案、用途以及设计原理做个归纳整理。

一、解决的问题

1.回归测试覆盖率

测试用例不足或者遗漏难以覆盖所有场景,导致回归测试费时费力,线上稳定存在隐患,通过真实流量录制在回归测试时进行覆盖。

  • 回归特定接口和链路
  • 回归特定业务场景
  • 全量回归特定业务线

2.与全链路压测闭环

解决全链路压测的数据准备问题,通过流量录制和回放系统与压测系统打通,形成从流量录制到压测闭环。

  • 定向录制某个链路接口线上流量
  • 对录制流量进行压测打标
  • 增压发起全链路压测

3.数据的其他用处

  • 抽取线上流量测试环境调试复现
  • 其他用到线上请求数据的地方
阅读全文 »

一、网络拓扑与流量走向

1.网络拓扑架构

下面是一个比较通用的南北流量网关部署架构,各个层次如下:

  • 终端服务层:公司提供的各种设备、APP等
  • 四层负载均衡集群:SLB/LVS等
  • 七层负载均衡集群:Nginx等,在这一层可以植入安全插件WAF等
  • 网关层:负责终端与内部服务通信协议转换、通知推送等
  • 后端服务:业务微服务应用

2.流量走向

从北向南

  • 终端通过HTTP/TPC/WebSocket等协议发送请求,网关接受请求解析数据包
  • 解析数据包通常会使用秘钥或者秘钥池
  • 解密后组装数据格式抽取映射标识(指令码或者action)
  • 根据业务配置的映射关系通过标识查询对应的后端服务接口与协议
  • 向后端微服务发起调用

从南到北

  • 业务处理完逻辑后向网关发起回调
  • 网关先查找该请求的长连接在哪台网关机器上
  • 找到与终端的长连接将回调的内容完成推送
阅读全文 »

前言

全链路观测平台设计离不开基础数据的采集、提炼和呈现。本文就基础数据日志、指标、链路的采集原理进行梳理,如何将其关联最终提供辅助决策价值提点归纳。

一、数据采集

1.日志架构简图

统一日志: 标准化日志格式、链路ID透传、自定义检索标识

日志类型: 应用日志、中间件日志(RPC框架、消息、缓存、存储等)、网关日志、终端日志

收集策略: 例如根据IP、APP、文件等灵活管控,不同日志分类管理

数据清洗: 清洗重复非标准数据、重复数据、聚合高质量数据

存储数据: 区分哪些数据适合ES、哪些数据适合ClickHouse、哪些数据适合时序库

性能成本: 延迟问题、查询性能、存储成本

小结: 通过标准化的日志格式,多样化的收集策略,清洗成高质量数据为根因定位提供基础保障。

2.链路架构简图

采样策略

  • 固定采样率:保持固定采样的频率
  • 最低采样率:过低流量保证最低的采样率
  • 自适应采样率:根据流量自动适应采样率
  • 全部采样率:对应特高优先流量100%采样
  • 染色采样:对于染色打标的请求100%采样
  • 应急采样:请求传递过程中检测到错误或者异常,强制将该请求采样

动态设置

  • 采样率采样策略动态调整
  • 自杀熔断保护 不允许过度占用资源影响业务

小结: 链路采集和分析关键的点在于如何提供灵活的采样策略,将核心链路、异常链路能实现高质量采集。

阅读全文 »

错误日志一

日志分析

收到业务同学反馈发现有SOA错误,但是对业务没有什么影响,错误内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
io.grpc.StatusRuntimeException: INTERNAL: HTTP/2 error code: PROTOCOL_ERROR
Received Goaway
Stream 99 does not exist
at io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:262)
at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java:243)
at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java:156)
at com.hellobike.soa.protobuf.internal.SoaInvokerServiceGrpc$SoaInvokerServiceBlockingStub.call(SoaInvokerServiceGrpc.java:222)
at com.hellobike.soa.rpc.invoker.v1.block.BlockingGrpcClientInvoker.invokeInternal(BlockingGrpcClientInvoker.java:31)
at com.hellobike.soa.rpc.invoker.v1.block.AbstractBlockingGrpcClientInvoker.doInvoke(AbstractBlockingGrpcClientInvoker.java:42)
at com.hellobike.soa.core.client.invoke.ClientInvoker.lambda$invoke$0(ClientInvoker.java:37)
at com.hellobike.otter.context.Context.supplier(Context.java:623)
at com.hellobike.soa.core.client.invoke.ClientInvoker.invoke(ClientInvoker.java:37)
at com.hellobike.soa.core.invoke.filter.FilterInvoker.invoke(FilterInvoker.java:42)
at com.hellobike.soa.core.client.filter.RouteFilter.invoke(RouteFilter.java:32)
at com.hellobike.soa.core.invoke.filter.FilterInvoker.invoke(FilterInvoker.java:43)
at com.hellobike.soa.core.client.filter.RateLimitExceptionFilter.invoke(RateLimitExceptionFilter.java:26)
at com.hellobike.soa.core.invoke.filter.FilterInvoker.invoke(FilterInvoker.java:43)
at com.hellobike.soa.core.client.filter.SentinelFilter.invoke(SentinelFilter.java:49)
at com.hellobike.soa.core.invoke.filter.FilterInvoker.invoke(FilterInvoker.java:43)
at com.hellobike.soa.core.client.filter.ServiceDowngradeFilter.invoke(ServiceDowngradeFilter.java:31)
at com.hellobike.soa.core.invoke.filter.FilterInvoker.invoke(FilterInvoker.java:43)
at com.hellobike.soa.core.invoke.filter.FilterChain.invoke(FilterChain.java:64)
at com.hellobike.soa.core.proxy.jdk.JDKProxyHandler.invokeInternal(JDKProxyHandler.java:73)
at com.hellobike.soa.core.proxy.jdk.JDKProxyHandler.invoke(JDKProxyHandler.java:44)

Goaway帧含义

先看下这个帧的含义:用于关闭连接或者发出错误, 端点必须将带有0x0以外的流标识符的GOAWAY帧视为类型为PROTOCOL_ERROR的连接错误

Goway帧抓包格式如下图所示:

小结:现象分析,该服务未客户端收到的HTTP/2二进制帧为Goaway,并抛出协议错误PROTOCOL_ERROR以及Stream 99 does not exist。

源码跟踪

先跟踪Stream 99 does not exist,在netty包DefaultHttp2ConnectionDecoder类中找到:

1
2
3
4
5
private void verifyStreamMayHaveExisted(int streamId) throws Http2Exception {
if (!connection.streamMayHaveExisted(streamId)) {
throw connectionError(PROTOCOL_ERROR, "Stream %d does not exist", streamId);
}
}

跟下streamMayHaveExisted方法逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean streamMayHaveExisted(int streamId) {
return remoteEndpoint.mayHaveCreatedStream(streamId) || localEndpoint.mayHaveCreatedStream(streamId);
}

// 判断服务端帧是否为合法帧:1.是否大于0 2.是否为服务端帧(2的倍数)3.是否为合法区间的帧
public boolean mayHaveCreatedStream(int streamId) {
return isValidStreamId(streamId) && streamId <= lastStreamCreated();
}

// 在HTTP/2中客户端发起的StreamID必须是奇数,服务器发起的StreamID必须是偶数
public boolean isValidStreamId(int streamId) {
return streamId > 0 && server == ((streamId & 1) == 0);
}

// 计算服务端合法帧区间
public int lastStreamCreated() {
return nextStreamIdToCreate > 1 ? nextStreamIdToCreate - 2 : 0;
}

看下哪里调用了streamMayHaveExisted方法,在shouldIgnoreHeadersOrDataFrame方法进行调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 private boolean shouldIgnoreHeadersOrDataFrame(ChannelHandlerContext ctx, int streamId, Http2Stream stream,
String frameName) throws Http2Exception {
if (stream == null) {
if (streamCreatedAfterGoAwaySent(streamId)) {
logger.info("{} ignoring {} frame for stream {}. Stream sent after GOAWAY sent",
ctx.channel(), frameName, streamId);
return true;
}

// Make sure it's not an out-of-order frame, like a rogue DATA frame, for a stream that could
// never have existed.
verifyStreamMayHaveExisted(streamId);
// ...
}

调用shouldIgnoreHeadersOrDataFrame的地方有onDataRead和onHeadersRead方法,分别解析请求的header和Data部分。

Data解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Handles all inbound frames from the network.
*/
private final class FrameReadListener implements Http2FrameListener {
@Override
public int onDataRead(final ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
boolean endOfStream) throws Http2Exception {
Http2Stream stream = connection.stream(streamId);
Http2LocalFlowController flowController = flowController();
int bytesToReturn = data.readableBytes() + padding;

final boolean shouldIgnore;
try {
// 调用点
shouldIgnore = shouldIgnoreHeadersOrDataFrame(ctx, streamId, stream, "DATA");
} catch (Http2Exception e) {
}

// ...
}

Header解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception {
Http2Stream stream = connection.stream(streamId);
boolean allowHalfClosedRemote = false;
if (stream == null && !connection.streamMayHaveExisted(streamId)) {
// 创建服务端Stream
stream = connection.remote().createStream(streamId, endOfStream);
// Allow the state to be HALF_CLOSE_REMOTE if we're creating it in that state.
allowHalfClosedRemote = stream.state() == HALF_CLOSED_REMOTE;
}
// 调用点
if (shouldIgnoreHeadersOrDataFrame(ctx, streamId, stream, "HEADERS")) {
return;
}
// ...
}

private void incrementExpectedStreamId(int streamId) {
if (streamId > nextReservationStreamId && nextReservationStreamId >= 0) {
nextReservationStreamId = streamId;
}
nextStreamIdToCreate = streamId + 2; // 服务端帧偶数递增数
++numStreams;
}

小结:

1.代码调用链条如下
onHeadersRead/onDataRead->shouldIgnoreHeadersOrDataFrame->streamMayHaveExisted->mayHaveCreatedStream->lastStreamCreated

2.从代码来看,在解析Header或者Data部分出现帧乱序,当前帧ID超过下一个帧预期的大小

3.疑问到底是解析header出现问题还是解析data?

错误日志二

1
2
3
4
5
6
7
8
9
10
11
12
Caused by: io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception$HeaderListSizeException: Header size exceeded max allowed size (8192)
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception.headerListSizeError(Http2Exception.java:189) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2CodecUtil.headerListSizeExceeded(Http2CodecUtil.java:233) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.HpackEncoder.encodeHeadersEnforceMaxHeaderListSize(HpackEncoder.java:133) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.HpackEncoder.encodeHeaders(HpackEncoder.java:117) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder.encodeHeaders(DefaultHttp2HeadersEncoder.java:74) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.DefaultHttp2FrameWriter.writeHeadersInternal(DefaultHttp2FrameWriter.java:501) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.DefaultHttp2FrameWriter.writeHeaders(DefaultHttp2FrameWriter.java:268) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2OutboundFrameLogger.writeHeaders(Http2OutboundFrameLogger.java:60) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.DecoratingHttp2FrameWriter.writeHeaders(DecoratingHttp2FrameWriter.java:53) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]
at io.grpc.netty.shaded.io.grpc.netty.NettyClientHandler$PingCountingFrameWriter.writeHeaders(NettyClientHandler.java:966) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]
at io.grpc.netty.shaded.io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder.sendHeaders(DefaultHttp2ConnectionEncoder.java:180) ~[grpc-netty-shaded-1.33.1.jar:1.33.1]

这个错误日志很明显,Header大小超过8KB,Header size exceeded max allowed size (8192),这个大小时gPRC设定的。

1
2
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
public static final int DEFAULT_MAX_HEADER_LIST_SIZE = 8192;

小结: 该错误为gRPC设置了Header大小为8KB,超过该大小具体错误是Netty抛出的。

源码跟踪

下面是报错的地方

1
2
3
4
5
public static void headerListSizeExceeded(int streamId, long maxHeaderListSize,
boolean onDecode) throws Http2Exception {
throw headerListSizeError(streamId, PROTOCOL_ERROR, onDecode, "Header size exceeded max " +
"allowed size (%d)", maxHeaderListSize);
}

最后跟踪写入header时进行的校验

1
2
3
4
 public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endStream, ChannelPromise promise) {
return delegate.writeHeaders(ctx, streamId, headers, padding, endStream, promise);
}

根因截图

通过解析内容发现在gRPC header部分传入了大的链路ID导致,需要重构该链路生产方案。

总结: 在gRPC通信时由于前面一条消息header头过大抛出异常Header size exceeded max allowed size (8192),导致后续帧发生乱序。

客户端AppTaxiNormalService调用服务Appxxx发生超时,长达50秒。

客户端等待中取消请求,发生调用时间为:2021-11-02 22:11:59.148

该服务基本上在同一时间段发起向下游的服务均发生超时。

队列显示瞬间增加很多任务

磁盘IO和CPU都有上升

线程dump情况,通信线程调用到了SynchronizationContext,底层的work通信线程怎么调用到了获取节点的业务方法去了。

代码中有使用SynchronizationContext

SynchronizationContext使用的queue是ConcurrentLinkedQueue队列,被单线程串行执行。

再回到上面的线程栈,业务节点发现事件和gRPC底层通信共用了SynchronizationContext造成阻塞,和线程错乱执行。

引言

通过开源同步工具NacosSync的分析,对我们实现自定义的同步工具提供参考。文本就同步任务分发与Nacos集群之间、从zk到Nacos的同步源码做个分析。

内容提要

任务和配置入库

  • 集群配置入库
  • 同步任务入库

同步任务分发

  • 每三秒调度一次任务列表
  • 新增任务发布同步任务事件SyncTaskEvent并由listenerSyncTaskEvent处理
  • 删除任务发布删除任务事件DeleteTaskEvent并由listenerDeleteTaskEvent处理
  • 任务的发布和订阅使用Guava的EventBus

Nacos集群之间同步逻辑

  • 两个Nacos集群之间进行同步,同步任务在Service维度(AppId)建立
  • 对源集群注册监听获取注册节点列表,通过剔除无效节点后,将新的节点注册到目标集群

从zk集群同步到Nacos集群

  • NacosSync从zk集群同步到Nacos只支持dubbo路径
  • 第一次先同步所有节点过去,再监听源集群路径变化,同步到目标集群
阅读全文 »

引言

Nacos的注册发现和配置中心的源码基本录完了,还有一块是不同集群之间的同步。zk同步到Nacos集群,nacos集群之间做多活需要数据复制等。那本文就先看下如何使用它,进而后面文章分析如何实现。

阅读全文 »