使用es作为日志存储,上手使用方便,但资源消耗大

冷热节点

定期将旧日志热节点移动到冷节点中存储,热节点配置高速度快,冷节点存储成本低,且支持低频率查询

使用node.attr.box_type: hot/cold标识冷热节点

存储:热节点使用ssd,冷节点使用高效云盘

配置参考

计算资源: 2 核 8GB 的节点可以支持 5000 qps 的写入,随节点数量和节点规格提升,写入能力基本呈线性增长

索引和分片数量: 一个 shard 的数据量在 30-50 GB为宜,1GB 堆内存支持 20-30 个分片为宜,集群总体的分片数量一般不要超过 3w

索引

shard数量: 一天单服务的日志最大在1g,链路追踪的日志最大为3g,shard设置为即可,后期考虑将服务日志做合并到一个索引,用appname区分

索引模板

创建索引模板

1
2
3
4
5
6
7
8
9
10
PUT _template/log_template
{
"template": "log_*”, // 匹配索引
"order": 98,//优先级 值越大越高
"settings": {
"index.number_of_shards": "1",
"index.refresh_interval": "5s",
"index.routing.allocation.require.box_type":"hot"
}
}

ILM策略

索引生命周期策略

warm阶段: 因为日志是按天生成,设置24小时后过渡到warm阶段,索引已不再写入,合并segment为1,其余不变,保障搜索性能

cold阶段: 系统要求90天内日志可查,所以设置90天后过渡到cold,并将节点设置为cold,同时副本数(index.number_of_replicas)改为0,并冻结索引(把索引常驻内存的一些数据从内存中清理掉(比如 FST , 元数据等),默认情况下无法查询已经冻结的索引,需要显式增加 “ignore_throttled=false” 参数)

旧索引不会自动加入ILM,需要手动修改(建议分批修改,否则master可能会oom)

1
2
3
4
PUT log_*/_settings
{
"index.lifecycle.name": "mylifecycle"
}

并且修改策略只对之后新创建的索引生效

快照

存储库:使用oss挂载到节点上作为共享存储

自动快照的索引选择不支持时间表达式,只能自己写脚本生成每月快照

1
2
3
4
5
6
7
8
9
10
POST /_snapshot/my_backup/my_snapshot?
{
"indices": "*-2020.11.*",
"ignore_unavailable": true, // 忽略不可用索引
"include_global_state": false, // 不包括全局状态
"metadata": {
"taken_by": "kimchy",
"taken_because": "backup before upgrading"
}
}

待快照完成后再删除索引

1
DELETE /*-2020.11.*

其他命令

集群配置

transient 临时:这些设置在集群重启之前一直会生效。一旦整个集群重启,这些设置就被清除

persistent 永久:这些设置永久保存,除非再次被手动修改。是将修改持久化到文件中,重启之后也不影响。

节点进行数据传输的每秒最大字节数,-1是指无限制,默认为40MB

1
2
3
4
5
6
PUT _cluster/settings
{
"transient": {
"indices.recovery.max_bytes_per_sec": "-1"
}
}

调整集群恢复时的单机并发度,默认是2

1
2
3
4
5
6
PUT _cluster/settings
{
"transient": {
"cluster.routing.allocation.node_concurrent_recoveries": 4
}
}

场景

业务:下单时,手术类型中的商品需要与授权商品取交集过滤,剩下的商品才能返回给前端展示

难点:某些大客户手术类型数量有两百多,每个又包含数百商品,需要和多达十万的商品循环做取交集运算

问题:原先使用的lodash的intersection方法循环每个手术类型取交集,在获取用户德高的手术类型时需要耗时2500ms

设手术类型*商品数量为G,授权商品数量为N,当前时间复杂度为GN

优化

使用二分法查找:先确定待查数据的范围,然后逐步缩小范围直到找到或找不到该记录为止,需要先将数组排序

有序数组

先将授权商品排序,使用快速排序(NlogN),再用二分查找搜索商品(logN)

合计时间复杂度为NlogN+GlogN,减去原先N*G得到NlogN+(logN-N)*G所以N,G越大时优化效果越明显

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function searchSortArray(sortArray,val){
function handle(left, right) {
if (left > right){
return false;
}
const mid = Math.floor((left + right ) / 2);
const midVal = sortArray[mid];
if(midVal===val){
return true;
}
if(midVal>val){
return handle(left,mid-1)
}
return handle(mid+1,right)
}
return handle(0, sortArray.length - 1);
}
二叉搜索树(BST)
  • BST:根节点的值大于其左子树中任意一个节点的值,小于其右节点中任意一节点的值

有序数组在计算时,每次都要计算一次中间值(logN),使用BST能省掉这次计算

再排序的基础上构建一个二叉搜索树(N),再检索二叉树(logN)

所以原先的有序数组时间复杂度为 NlogN+2GlogN,二叉树的时间复杂度为NlogN+GlogN+N,相减得到GlogN-N,当G较大时使用BST效果好

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
26
27
28
29
30
31
32
33
// 二叉树结构
function TreeNode(val, left, right) {
this.val = (val === undefined ? 0 : val);
this.left = (left === undefined ? null : left);
this.right = (right === undefined ? null : right);
}
// 有序数组转BST
function arrayToBST(sortArray) {
function handle(left, right) {
if (left > right){
return null;
}
const mid = Math.floor((left + right ) / 2);
const root = new TreeNode(sortArray[mid]);
root.left = handle(left, mid - 1);
root.right = handle(mid + 1, right);
return root;
}
return handle(0, sortArray.length - 1);
}
// 检索BST
function searchBST(BST, val) {
if (!BST) return false;
if (val === BST.val) {
return true;
}
if (val < BST.val) {;
return searchBST(BST.left, val);
}
return searchBST(BST.right, val);
}

结果

在使用BST优化后时间降为480ms,比原先快了5倍,效果显著

在实际业务处理中要对商品数量做判断后再选择合适的算法

问题排查

常见问题

  • 逻辑缺陷:e.g. NPE、死循环、边界情况未覆盖。、
  • 性能瓶颈:e.g. 接口 RT 陡增、吞吐率上不去。
  • 内存异常:e.g. GC 卡顿、频繁 FGC、内存泄露、OOM
  • 并发/分布式:e.g. 存在竞争条件、时钟不同步。
  • 数据问题:e.g. 出现脏数据、序列化失败。
  • 安全问题:e.g. DDoS 攻击、数据泄露。
  • 环境故障:e.g. 宿主机宕机、网络不通、丢包。
  • 操作失误:e.g. 配置推错、删库跑路(危险动作,请勿尝试..)。

排查流程

快速修复

  • 发布期间开始报错,且发布前一切正常? 先回滚再说,恢复正常后再慢慢排查
  • 应用已经稳定运行很长一段时间,突然开始出现进程退出现象?很可能是内存泄露,默默上重启大法吧
  • 只有少数固定机器报错?试试隔离这部分机器(关闭流量入口)。
  • 单用户流量突增导致服务不稳定?如果不是惹不起的金主爸爸,请勇敢推送限流规则。
  • 下游依赖挂了导致服务雪崩?还想什么呢,降级预案走起。

保留现场

  • 隔离一两台机器:将这部分机器入口流量关闭,让它们静静等待你的检阅。
  • Dump 应用快照:常用的快照类型一般就是线程堆栈和堆内存映射。

所有机器都回滚了,咋办?别慌,如果你的应用监控运维体系足够健全,那么你还有多维度的历史数据可以回溯:应用日志、中间件日志、GC 日志、内核日志、Metrics 指标等。

定位原因

  • 关联近期变更:90% 以上的线上问题都是由变更引发,这也是为什么集团安全生产的重点一直是在管控“变更”。所以,先不要急着否认(“肯定不是我刚加的那行代码问题!”),相信统计学概率,好好 review 下近期的变更历史(从近至远)。

  • 全链路追踪分析:微服务和中台化盛行的当下,一次业务请求不经过十个八个应用处理一遍,都不好意思说自己是写 Java 的。所以,不要只盯着自己的应用不放,你需要把排查 scope 放大到全链路。

  • 还原事件时间线:请把自己想象成福尔摩斯(柯南也行),摆在你面前的就是一个案发现场,你需要做的是把不同时间点的所有事件线索都串起来,重建和还原整个案发过程。要相信,时间戳是不会骗人的。

  • 找到 Root Cause:排查问题多了你会发现,很多疑似原因往往只是另一个更深层次原因的表象结果之一。作为福尔摩斯,你最需要找到的是幕后凶手,而不是雇佣的杀人犯 —— 否则 TA 还会雇人再来一次。

  • 尝试复现问题:千辛万苦推导出了根因,也不要就急着开始修 bug 了。如果可以,最好能把问题稳定复现出来,这样才更有说服力。这里提醒一点:可千万别在生产环境干这事(除非你真的 know what you’re doing),否则搞不好就是二次伤害(你:哈哈哈,你看,这把刀当时就是从这个角度捅进去的,轨迹完全一样。用户:…)。

解决问题

  • 修复也是一种变更,需要经过完整的回归测试、灰度发布;切忌火急火燎上线了 bugfix,结果引发更多的 bugs to fix。

  • 修复发布后,一定要做线上验证,并且保持观察一段时间,确保是真的真的修复了。

  • 最后,如果问题已经上升到了故障这个程度,那就拉上大伙好好做个故障复盘吧。整个处理过程一定还有提升空间,你的经验教训对其他同学来说也是一次很好的输入和自查机会:幸福总是相似的,故障也是。

排查⼯具

日志(Logging)、监控(Metrics)、追踪(Tracing)

系统优化

  • 系统优化的三个基本方向:性能(Performance)、稳定性(Stability)、可维护性(Maintainability)。三者之间并不是完全独立的,而是存在着复杂的相互作用关系,有时甚至会此消彼长。

性能优化

性能指标

指标(Indicators)是衡量一件事物好坏的科学量化手段。对于性能而言,一般会使用如下指标评估:

  • 吞吐率(Throughput):系统单位时间内能处理的工作负载,例如:在线 Web 系统 - QPS/TPS,离线数据分析系统 - 每秒处理的数据量。

  • 响应时间(Response Time):以 Web 请求处理为例,响应时间(RT)即请求从发出到收到的往返时间,一般会由网络传输延迟、排队延迟和实际处理耗时几个部分共同组成。

  • 可伸缩性(Scalability):系统通过增加机器资源(垂直/水平)来承载更多工作负载的能力;投入产出比越高(理想情况是线性伸缩),则说明系统的可伸缩性越好。

此外,同一个系统的吞吐率与响应时间,一般还会存在如下关联关系:吞吐率小于某个临界值时,响应时间几乎不变;一旦超出这个临界值,系统将进入超载状态(overloaded),响应时间开始线性增长。对于一个有稳定性要求的系统,需要在做性能压测和容量规划时充分考虑这个临界值的大小。

性能分析

坚持 2/8 原则:优先分析和优化系统瓶颈,即当前对系统性能影响最大的原子操作;他们很可能就是 ROI 最高的优化点。

系统层面:tsar、top、iostat、vmstat
网络层面:iftop、tcpdump、wireshark
数据库层面:SQL explain、CloudDBA
应用代码层面:JProfiler、Arthas、jstack

其中很多工具也是问题排查时常用的诊断工具;毕竟,无论是性能分析还是诊断分析,目的都是去理解一个系统和他所处的环境,所需要做的事情都是相似的。

优化原则

性能优化与做功能需求一样,都是为业务服务的,因此优化时千万不要忙着自嗨,一定要结合目标需求和应用场景 —— 也许这块你想做的优化,压根线上就碰不到;也许那块很难做的优化,可以根据流量特征做非通用的定制优化

你不应该做的:即老生常谈的提前优化(Premature-optimization)与过度优化(Over-optimization) —— 通常而言(并不绝对),性能优化都不是免费的午餐,优化做的越多,往往可维护性也会越差。

优化手段
简化
  • 业务层面:e.g. 流程精简、需求简化。
  • 编码层面:e.g. 循环内减少高开销操作。
  • 架构层面:e.g. 减少没必要的抽象/分层。
  • 数据层面:e.g. 数据清洗、提取、聚合。
并行

方式:单机并行(多线程)、多机并行(分布式)。

优点:充分利用机器资源(多核、集群)。

缺点:同步开销、线程开销、数据倾斜。

同步优化:乐观锁、细粒度锁、无锁。
线程替代(如协程:Java WISP、Go routines、Kotlin coroutines)。
数据倾斜:负载均衡(Hash / RR / 动态)

异步

方式:消息队列 + 任务线程 + 通知机制。

优点:提升吞吐率、组件解耦、削峰填谷。

缺点:排队延迟(队列积压)。

避免过度积压:Back-pressure(Reactive思想)。

批量

有些事,你可以合起来一起做。

方式:多次单一操作 → 合并为单次批量操作。

案例:TCP Nagel 算法;DB 的批量读写接口。

优点:避免单次操作的固有开销,均摊后总开销更低。

缺点:等待延迟 + 聚合延迟。

减少等待延迟:Timeout 触发提交,控制延迟上限。

时间空间互换

游戏的本质:要么有闲,要么有钱。

空间换时间:避免重复计算、拉近传输距离、分流减少压力。

案例:缓存、CDN、索引、只读副本(replication)。

时间换空间:有时候也能达到“更快”的效果(数据量减少 → 传输时间减少)。

案例:数据压缩(HTTP/2 头部压缩、Bitmap)。

数据结构与算法优化

程序 = 数据结构 + 算法

多了解一些“冷门”的数据结构 :Skip list、Bloom filter、Time Wheel 等。

一些“简单”的算法思想:递归、分治、贪心、动态规划。

池化 & 局部化

共享经济 & 小区超市

池化(Pooling):减少资源创建和销毁开销。

案例:线程池、内存池、DB 连接池、Socket 连接池。

局部化(Localization):避免共享资源竞争开销。

案例:TLB(ThreadLocalBuffer)、多级缓存(本地局部缓存 -> 共享全局缓存)。

更多优化手段

升级红利:内核、JRE、依赖库、协议。
调参大师:配置、JVM、内核、网卡。
SQL 优化:索引、SELECT *、LIMIT 1。
业务特征定制优化:e.g. 凌晨业务低峰期做日志轮转。
Hybrid 思想(优点结合):JDK sort() 实现、Weex/RN。

稳定性优化

稳定是相对的,业务规模越大、场景越复杂,系统越容易出现不稳定,且带来的影响也越严重

衡量指标

定义服务的可用跟业务相关,越是底层的基础设施,可用性要求就越高

定义服务的可用性:服务总的可用/不可用比例(服务时长 or 服务次数),监测和量化一个系统的稳定性

可用性:

  • 服务可访问: 接口返回200 OK
  • 性能可接受: 99%的请求RT<50ms

商业化指标:

  • SLI:
  • SLO:
  • SLA:

可用性测量

探针模拟: 从客户端侧,模拟用户的调用行为

  • 优点:数据真实(客户端角度)
  • 缺点:数据不全面(单一客户数据)

服务端采集: 从服务端侧,直接分析日志和数据

  • 优点:覆盖所有调用数据
  • 缺点:缺失客户端链路数据

稳定性优化原则

关注 RT 的数据分布(如:p50/p99/p999 分位点),不要尝试承诺和优化可用性到 100%

稳定性优化手段
避免单点
  • 集群部署
  • 数据副本
  • 多机房容灾

只堆量不够,还需要具备故障转移能力(Failover)。

接入层:DNS、VipServer、SLB。

服务层:服务发现 + 健康检查 + 剔除机制。

应用层:无状态设计(Stateless),便于随时和快速切换。

流控/限流

类型:QPS 流控、并发度流控。

工具:RateLimiter、信号量、Sentinel。

粒度:全局、用户级、接口级。

热点流控:避免意料之外的突增流量。

熔断

目的:防止连锁故障(雪崩效应),及时止损

工具:Hystrix、Failsafe、Resilience4j。

功能:自动绕开异常服务并检测恢复状态。

流程:关闭 → 打开 → 半开。

降级

触发原因:流控、熔断、负载过高。

常见降级方式:

关闭非核心功能:停止应用日志打印

牺牲数据时效性:返回缓存中旧数据

牺牲数据精确性:降低数据采样频率

超时/重试

超时:避免调用端陷入永久阻塞。

超时时间设置:全链路自上而下规划

Timeout vs. Deadline:使用绝对时间会更好

重试:确保可重试操作的幂等性。

消息去重

异步重试

指数退避

资源设限

目的:防止资源被异常流量耗尽

资源类型:线程、队列、DB 连接

设限方式:资源池化、有界队列

超限处理:返回 ServiceUnavailable / QuotaExceeded

资源隔离

目的:防止资源被部分异常流量耗尽;为 VIP 客户提供服务质量保证(QoS)。

隔离方式:队列划分、独立集群;注意处理优先级和资源分配比例。

安全生产

程序动态性:开关、配置、热升级。

Switch:类型安全;侵入性小。

审核机制:代码 Review、发布审批。

灰度发布;分批部署;回滚预案。

DUCT:自动/手动调整 HSF 节点权重。

可维护性优化

相比性能和稳定性而言,可维护性所体现的价值往往是最长远、但也最难在短期内可兑现的

  • 软件生命周期:维护周期 >> 开发周期。

  • 破窗效应、熵增定律:可维护性会趋向于越来越差。

  • 遗留系统的危害:理解难度,修改成本,变更风险;陷入不断踩坑、填坑、又挖坑的循环。

可维护性衡量指标

  • 复杂度(Complexity):是否复杂度可控?

编码:简洁度、命名一致性、代码行数等。

架构:组件耦合度、层次清晰度、职责单一性等。

  • 可扩展性(Extensibility):是否易于变更?

需要变更代码或配置时,是否简单优雅、不易出错。

  • 可运维性(Operability):是否方便运维?

日志、监控是否完善;部署、扩容是否容易。

可维护性优化原则

  • 遵循 KISS 原则、DRY 原则、各种代码可读性和架构设计原则
  • 不应该引入过多临时性、Hack 代码;功能 Work 就 OK,欠一堆技术债

可维护性优化手段

编码规范

编码:推荐《Java 开发手册》,另外也推荐 The Art of Readable Code 这本书。ESLint

日志:无盲点、无冗余、TraceID。

测试:代码覆盖度、自动化回归。

代码重构

何时重构:任何时候代码中嗅到坏味道(bad smell)。

重构节奏:小步迭代、回归验证。

重构 vs. 重写:需要综合考虑成本、风险、并行版本维护等因素。

数据驱动

系统数据:监控覆盖、Metrics 采集等,对于理解系统、排查问题至关重要。

业务数据:一致性校验、旧数据清理等;要相信,数据往往比代码要活得更久。

技术演进

需要综合评估风险、生产力、学习成本。

当前方向:微服务化、容器化。

当共享资源自身无法提供互斥能力的时候,为了避免多机器同时读写访问造成数据破坏,就需要一个第三方服务提供互斥锁,获取到锁的机器就可以排他性的访问共享资源,这样的服务我们统称为分布式锁服务,锁也就叫分布式锁

锁 = 资源 + 并发控制 + 所有权展示

分布式锁的分类

异步复制

  • 基于异步复制的分布式系统,例如 mysql,tair,redis 等

存在数据丢失(丢锁)的风险,不够安全,往往通过 TTL 的机制承担细粒度的锁服务

该系统接入简单,适用于对时间很敏感,期望设置一个较短的有效期,执行短期任务,丢锁对业务影响相对可控的服务

一致性协议

  • 基于 paxos 协议的分布式一致性系统,例如 zookeeper,etcd,consul 等

通过一致性协议保证数据的多副本,数据安全性高,往往通过租约(会话)的机制承担粗粒度的锁服务

该系统需要一定的门槛,适用于对安全性很敏感,希望长期持有锁,不期望发生丢锁现象的服务

分布式锁的实践

提升分布式锁使用时的正确性、保证锁的可用性以及提升锁的切换效率
x

互斥性

每把锁都和唯一的会话绑定,客户端通过定期发送心跳来保证会话的有效性,也就保证了锁的拥有权。当心跳不能维持时,会话连同关联的锁节点都会被释放,锁节点就可以被重新抢占

全局锁服务提供全局自增的 token,Client 1 拿到锁返回的 token 是 33,并带入存储系统,发生 GC,当 Client 2 抢锁成功返回 34,带入存储系统,存储系统会拒绝 token 较小的请求,那么经过了长时间 full gc 重新恢复后的 Client 1 再次写入数据的时候,因为存储层记录的 token 已经更新,携带 token 值为 33 的请求将被直接拒绝,从而达到了数据保护的效果(像paxos中通过提案)

可用性

通过持续心跳来保证锁的健壮性

处理异常的用户进程持续占据锁: 会话加黑机制,不再维护心跳,最终导致会话过期。

不能强制删除锁:

  1. 删除锁操作本身不安全,如果锁已经被其他人正常抢占,此时删锁请求会产生误删除。

  2. 删除锁后,持有锁的人会话依然正常,它仍然认为自己持有锁,会打破锁的互斥性原则。

切换效率

当进程持有的锁需要被重新调度时,持有者可以主动删除锁节点

但当持有者发生异常(如进程重启,机器宕机等),新的进程要重新抢占,就需要等待原先的会话过期后,才有机会抢占成功

  1. 要提升切换精度,本质上要压缩会话生命周期,同时也意味着更快的心跳频率

  2. 守护进程发现锁持有进程挂掉的场景,提供锁的 CAS 释放操作,使得进程可以零等待进行抢锁,比如利用在锁节点中存放进程的唯一标识,强制释放已经不再使用的锁,并重新争抢

现象

时间: 2020年8月30日 4点10分

es中所有open的日志全被删掉,包括系统日志,只留下一个read_me

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
26
27
28
29
30
31
32
33
34
35
36
37
{
"read_me": {
"aliases": {},
"mappings": {
"_doc": {
"properties": {
"@timestamp": {
"type": "date"
},
"message": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
},
"settings": {
"index": {
"creation_date": "1598760619467",
"number_of_shards": "5",
"number_of_replicas": "1",
"uuid": "cL_kgJ_XQqe2lSgOrN4VUw",
"version": {
"created": "6030299"
},
"provided_name": "read_me"
}
}
}
}
"message": "SEND 0.015 BTC TO THIS WALLET: 1BJxnAYaaWNbQcL9o9GS768v3VqVMNDusD IF YOU WANT RECOVER YOUR DATABASE! SEND TO THIS EMAIL YOUR SERVER IP AFTER SENDING THE BITCOINS getbase@cock.li"

网络i/o没有突增,所以数据并没有被移走,而是直接被删掉了

影响

数据丢失:es所在云盘有快照备份,可以从28号早上五点的恢复,丢失数据即28,29号和30号一部分,周末基本无日志,所以主要损失28号当前的请求日志

修复成本: 导出30号之后的日志,然后回滚磁盘,操作简单但耗时较长,预计影响时间3小时,后期考虑是否更换云盘类型,提高备份和回滚速度

漏洞查找

  • es没有密码保护

操作es有三种个途径,一是ip直接访问,二是通过内网slb访问,三是使用kibana

这三者都在内网中需要ecs安全组访问白名单,检查不存在大范围的ip授权,除了阿里云自动添加的ip段较大(到24位),但平时添加公司ip过期没有及时删除

检索slb中与发生时间相近的日志,条件过滤request_method: DELETE,查到了大量删除请求,再根据http_user_agent: python-requests/2.18.4为条件过滤得到了完整的攻击请求

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
26
27
28
29
30
31
32
33
34
35
36
37
08-28 23:38:25
client_ip: 195.54.161.252
client_port: 60516
http_host: *.*.*.*:9200
http_user_agent: python-requests/2.18.4
request_method: HEAD
request_uri: /
slb_vport: 9200
time: 2020-08-28T23:38:25+08:00
upstream_addr: *.*.*.*:32101
vip_addr: *.*.*.*
08-28 23:38:28
client_ip: 195.54.161.252
client_port: 39732
http_host: *.*.*.*:9200
http_user_agent: python-requests/2.18.4
request_method: GET
request_uri: /_all/
slb_vport: 9200
time: 2020-08-30T12:10:19+08:00
upstream_addr: *.*.*.*
vip_addr: *.*.*.*
08-28 23:38:36
client_ip: 195.54.161.252
client_port: 32844
http_host: *.*.*.*:9200
http_user_agent: python-requests/2.18.4
request_method: DELETE
request_uri: /log_saas_develop-2020.07.21/
slb_vport: 9200
time: 2020-08-28T23:38:36+08:00
upstream_addr: *.*.*.*:32101
vip_addr: *.*.*.*
...

一共发起了两次攻击,第一次发生在08-28 23:38:25,第二次是在08-30 12:10:19

http_host是一个公网ip,一开始在slb和ecs中都没查到,后来发现是我使用的阿里云子账号权限不够看不到EIP =_=||

漏洞: 内网slb绑定了公网eip,没有访问控制,也没有改es默认端口,被暴利扫描的脚本发现

问题处理

出现问题的原因: 原先es绑定的内网slb是单纯为es使用端口设为默认9200,方便内网中其他服务通过域名访问到es,后被合并到slb ops-k8s-cluster,但该slb是绑定在k8s上并提供了EIP给k8s的api server,导致es暴露到公网中,没有访问控制且未修改端口

处理方法: 增加访问控制,采用和ecs相同的方式刷白名单,不使用默认端口9200

总结

未被保护的资产暴露到公网被暴力扫描脚本攻击

发现问题与自检

  1. 暴露公网ip的服务(slb,ecs,rds)必须配置访问控制
  2. 自建的公共服务(es,redis,gitlab,jenkins,spinnaker等)禁止使用默认端口
  3. 自建服务只能通过域名访问,避免无访问记录
  4. 云盘快照备份中只备份工作日的配置不合理,原先是周一到周五,需改为周二到周六
  5. 定期更换服务密码

任务

记录

从mq获取任务,再记录到job表,记录的信息有 funcName,args,createUser,priority,serviceName

读取

从job表读取任务

限流: 当待执行队列长度超missionQueueSize则等待readTimeDelayMillsWhenFlowControl后重新执行

限频: 每隔readInterval读取任务

执行

从待执行队列中取出任务发送给worker

并发:根据parallelConsumeLimit限制,同时发送任务

当无任务时等待get_new_mission事件触发

发送任务时不等待结果,然后修改job表中任务状态为E

完成

监听etcd完成任务的信息写入

按执行结果修改job表中任务状态,从missionInProcessMap中删除该任务

检查

定期检查执行中的任务状态,主要是执行时间

对于执行missionOutT重试超过限制ime超时的任务重新加入任务队列,重试超次数retryTimes的任务则在job表中标记为F,失败原因为重试超过限制

master

etcd

将任务完成信息同步到etcd中
优势:

  1. 主动通知任务完成,实现方便,无需master等待完成结果
  2. 避免进度丢失,单点问题
  3. 可以查询记录
  4. master选举
master选举

key为/mif/schedule/leader,当版本为0时创建并设置ttl,其他scheduler监听该key,当失效时重新选举

初始化

service/job/init

  1. 读取etcd中已存在的任务(已完成master未处理)
  2. 从job表载入正在执行的任务,加入missionInProcessMap
  3. 启动监听etcd任务完成put事件
  4. 执行任务检查
  5. 执行任务完成
  6. 开始循环读取任务,检查任务,执行任务

发送任务

通过http post触发部署在函数计算中的worker

  • fc判断服务启动完成存在问题,在启动未完成时接受请求返回502

当请求返回此错误时,采取重试

1
2
3
4
5
if (response.data.errorMessage.indexOf('Process exited unexpectedly before completing request') !== -1) {
const { missionId, serviceName, funcName, args } = data;
app.logger.error(error);
return app.send(missionId, serviceName, funcName, args);
}

worker

部署在函数计算,节省资源,方便扩容,无需健康检查

接收任务与rpc处理逻辑类似不再重复
·

中间件finishMission

待执行完成后向etcd插入结果

1
2
3
4
5
6
7
await next();
if (!ctx.missionId) return;
await ctx.app.etcdMission.put('finish/' + ctx.missionId).value(JSON.stringify({
missionId: ctx.missionId,
time: Date.now(),
status: 'S',
}));

webpack配置

  • 将工程打包到一个文件中

工程中使用app-module-path设置src为根路径,所以在打包时需要对src做替换

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
26
27
28
const path = require('path');
module.exports = {
entry: './index.js',
target: 'node',
output: {
filename: 'bundle.js',
path: __dirname
},
// devtool: 'inline-source-map',
externals: _externals(),
resolve:{
alias: {
config: path.resolve(__dirname, 'config.js'),
'src': path.resolve(__dirname, './src'),
}
}
};
function _externals() {
let manifest = require('./package.json');
let dependencies = manifest.dependencies;
let externals = {};
for (let p in dependencies) {
externals[p] = 'commonjs ' + p;
}
return externals;
}

动态引用转为静态引用脚本

工程通过读取目标来实现动态引用,只在执行时触发,导致两个问题:

  1. 动态路径无法被webpack打包
  2. 转成静态引用后出现循环调用问题

生成静态引用脚本

生成对应目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const dir = dirname(__dirname)+'/src/routes';
let requireString=`'use strict';
`
fs.readdirSync(dir).filter(function (file) {
return (file.indexOf('.') !== 0) && (file !== 'index.js') && (file.slice(-3) === '.js');
})
.forEach(function (file) {
requireString += `require('./${basename(file)}');\n`
});
fs.writeFileSync(dir+'/index.js',requireString)

循环调用

工程service文件夹内部有相互调用,但直接引用index.js文件,转成静态引用后出现循环调用导致报错

通过Proxy来处理循环引用,原理是在init时只给个proxy对象,在正真调用时返回require对应的文件

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = new Proxy({}, {
get: (target, value) => {
// 有些地方在init时就要获取具体的service (like, {foo} = service),所以需要多返回一层Proxy
return new Proxy({
service: value
}, {
get: (target, value) => {
return requireObj[target.service][value]
}
});
}
})

.env文件处理

增加另外的启动文件run.js,加.env加到环境变量中

1
2
3
4
5
6
7
8
'use strict';
require('dotenv').config({path:__dirname+'/.env'})
process.env.APP_HOME = __dirname;
process.env.pkg = 1;
require('./bundle.js');

pkg编译

run.js设为编译入口,再引入.env和grpc.proto,node_modules也会打包编译进去

1
2
3
4
5
6
// package.json
"bin": "run.js",
"pkg":{
"assets": [".env","grpc.proto"]
}

最后生成saas的执行文件

分布式事务是指是指事务的发起者、参与者、数据资源服务器以及事务管理器分别位于分布式系统的不同节点之上
参与者通过网络通信来达到分布式一致性,网络通信不可避免出现失败、超时的情况

XA Specification

基于资源层的底层分布式事务解决方案,对业务的入侵度较低

有些数据分片框架或者中间件也支持XA协议,兼容性、普遍性好 但并发性能比较差, 基于XA的分布式事务如果要严格保证ACID,实际需要事务隔离级别为SERLALIZABLE

基于消息的分布式事务

通过消息系统来通知其他事务参与方自己事务的执行状态,有效的将事务参与方解耦,各个参与方可以异步执行

难点: 解决本地事务执行和消息发送的一致性:两者要同时执行成功或者同时取消执行

场景: 原则上只接受下游分支事务的成功,不接受事务的回滚,如果失败就要一直重试,适用于对最终一致性敏感度较低的业务场景

可能会因为接收方的消息堆积导致长时间的数据不一致

基于事务消息

事务消息发送成功后,处于 prepared 状态,不能被订阅者消费,等到事务消息的状态更改为可消费状态后,下游订阅者才可以监听到次消息。支持事务消息的 MQ 系统有一个定时扫描逻辑,扫描出状态仍然是“待发送”状态的消息,并向消息的发送方发起询问,询问这条事务消息的最终状态如何并根据结果更新事务消息的状态。

提供事务消息状态查询接口

基于本地消息

如果所依赖的 MQ 系统不支持事务消息,那么可以采用本地消息的分布式模式

事务的发起方维护一个本地消息表,业务执行和本地消息表的执行处在同一个本地事务中。业务执行成功,则同时记录一条“待发送”状态的消息到本地消息表中。系统中启动一个定时任务定时扫描本地消息表中状态为“待发送”的记录,并将其发送到 MQ 系统中,如果发送失败或者超时,则一直发送,知道发送成功后,从本地消息表中删除该记录

最大努力通知型分布式事务

基于 MQ 系统的一种解决方案,但是不要求 MQ 消息可靠(如支付宝支付成功通知),通过引入定期校验机制来对最终一致性做兜底,对业务侵入性较低、对 MQ 系统要求较低,实现比较简单,适合于对最终一致性敏感度比较低、业务链路较短的场景,比如跨平台、跨企业的系统间的业务交互

基于补偿的事务

事务补偿机制 : 在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务

TCC

将资源层的二阶段提交协议转换到业务层,成为业务模型中的一部分,核心思想是通过对资源的预留,尽早释放对资源的加锁,如果事务可以提交,则完成对预留资源的确认,如果事务要回滚,则释放预留的资源

业务设计和代码都会变复杂(需要业务方把功能的实现上由一个接口拆分为三个),但性能、隔离性都很好

业务方在设计实现上要遵循三个策略(网络中的通信失败或超时)

  1. 允许空回滚:try 失败或者没有执行 try 操作的参与方收到 cancel 请求时,要进行空回滚操作
  2. 保持幂等性:重复调用参与方的 confirm/cancel 方法,因此需要这两个方法实现上保证幂等性
  3. 防止资源悬挂:参与方侧 try 请求比 cancel 请求更晚到达的情况,cancel 会执行空回滚而确保事务的正确性,但是此时 try 方法也不可以再被执行

支持 TCC 事务的开源框架有:ByteTCC、Himly、TCC-transaction。

Saga模式

Saga 和 TCC 一样,也是一种补偿事务,但是它没有 try 阶段,而是把分布式事务看作一组本地事务构成的事务链

事务链中的每一个正向事务操作,都对应一个可逆的事务操作。Saga 事务协调器负责按照顺序执行事务链中的分支事务,分支事务执行完毕,即释放资源。如果某个分支事务失败了,则按照反方向执行事务补偿操作。如果补偿失败了,就一直重试,补偿操作可以优化为并行执行

不保证事务隔离性,本地事务提交后变更就对其他事务可见了。其他事务如果更改了已经提交成功的数据,可能会导致补偿操作失败。比如扣款失败,但是钱已经花掉了,业务设计上需要考虑这种场景并从业务设计上规避这种问题

不完美补偿,补偿操作会留下之前原始事务操作的痕迹,需要考虑对业务上的影响

要求业务设计实现上遵循三个策略:

  1. 允许空补偿:网络异常导致事务的参与方只收到了补偿操作指令,因为没有执行过正常操作,因此要进行空补偿
  2. 保持幂等性:事务的正向操作和补偿操作都可能被重复触发,因此要保证操作的幂等性
  3. 防止资源悬挂:网络异常导致事务的正向操作指令晚于补偿操作指令到达,则要丢弃本次正常操作,否则会出现资源悬挂问题

适合于业务流程长的长事务的场景,实现上对业务侵入低,所以非常适合微服务架构的场景。同时 Saga 采用的是一阶段提交模式,不会对资源长时间加锁,不存在“木桶效应”,所以采用这种模式架构的系统性能高、吞吐高

阿里云分布式事务相关工具

DRDS 基于MySQL的XA实现,使用方便,但并发性能低

GTS 全局事务服务 就提了个本地事务和mq能在一起提交

seata

通过全局锁实现了写隔离与读隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁
  • 拿不到 全局锁 ,不能提交本地事务。
  • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁

开启本地事务,拿到本地锁,更新操作,本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。第二阶段提交释放 全局锁

全局锁相当于写锁,但比由数据库实现性能好一些,
Seata(AT 模式)的默认全局隔离级别是 读未提交,会出现脏读,要拿读已提交需要全局锁,可以按业务做调整,但性能会好一些。全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时

性能比XA好一点,第一阶段的提交会释放本地锁,但还存在全局锁,会阻塞写,但不影响默认的读未提交
XA的prepare是数据库实现的锁,而且要求事务串行执行

规模控制

  • 规模控制既是技术工作的一部分,也是管理工作的一部分

把系统划分成若干部分,并设定每个部分的规模目标,对规模-速度权衡的各种情况有深刻的了解,并预留一些空间

制订总体规模的预算:仅对核心程序设定规模目标是不够的,必须把所有的方面都编入预算

明确模块的功能: 避免相互推脱

确保系统完整性、易用性:培养开发人员从系统整体出发、面向用户的态度是软件编程管理人员最重要的职能,加强成员相关的沟通,避免每个人都只在局部优化自己的程序

空间技能

更多的功能意味着需要更多的空间

可以设计成拥有若干选项 分组,根据选项组来剪裁程序,但也要确定用户可选项目的粗细程度(实现成本)

培训:快速的学习和经验的广泛共享
公共组件:运行速度较快和短小精炼的,与系统设计工作并行进行

数据的表现形式是编程的根本

战略上突破常来自数据或表的重新表达(授权数据的重新设计)

回顾、 分析实际情况,仔细思考程序的数据

原因

  1. 封装http版本的包,现有的js tcp版包已不再更新,有兼容问题

设计点记录